Skip to main content

supercode_opencode_frontend/
server.rs

1//! Authenticated numeric-loopback HTTP host for the stock OpenCode adapter.
2
3use std::fmt;
4use std::net::{IpAddr, Ipv4Addr, SocketAddr};
5use std::path::PathBuf;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::Arc;
8use std::time::Duration;
9
10use base64::Engine as _;
11use serde_json::Value;
12use supercode::SdkRuntime;
13use tokio::io::{AsyncReadExt, AsyncWriteExt};
14use tokio::net::{TcpListener, TcpStream};
15use tokio::sync::watch;
16use tokio::task::JoinSet;
17use zeroize::Zeroize;
18
19use crate::{OpenCodeAdapter, OpenCodeRequest, ResponseBody};
20
21const CREDENTIAL_BYTES: usize = 32;
22const MAX_HEADER_BYTES: usize = 64 * 1024;
23const MAX_BODY_BYTES: usize = 1024 * 1024;
24const MAX_EVENT_BYTES: usize = 16 * 1024 * 1024;
25const MAX_CONCURRENT_CONNECTIONS: usize = 32;
26const READ_TIMEOUT: Duration = Duration::from_secs(5);
27const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(15);
28
29#[derive(Debug, thiserror::Error)]
30pub enum CredentialError {
31    #[error("operating system random source failed")]
32    RandomSource,
33}
34
35/// Opaque stock-client password. It is non-cloneable, redacted, and zeroized;
36/// the only production disclosure path installs it in one child environment.
37pub struct OpenCodeClientCredential {
38    password: [u8; CREDENTIAL_BYTES],
39}
40
41impl OpenCodeClientCredential {
42    pub fn spawn_tokio_child(
43        &self,
44        command: &mut tokio::process::Command,
45    ) -> std::io::Result<tokio::process::Child> {
46        let mut password = encode_hex(&self.password);
47        command.env("OPENCODE_SERVER_PASSWORD", &password);
48        command.env("OPENCODE_SERVER_USERNAME", "opencode");
49        let child = command.spawn();
50        command.env_remove("OPENCODE_SERVER_PASSWORD");
51        command.env_remove("OPENCODE_SERVER_USERNAME");
52        password.zeroize();
53        child
54    }
55
56    #[cfg(test)]
57    fn authorization_header(&self) -> String {
58        let mut password = encode_hex(&self.password);
59        let encoded =
60            base64::engine::general_purpose::STANDARD.encode(format!("opencode:{password}"));
61        password.zeroize();
62        format!("Basic {encoded}")
63    }
64}
65
66impl fmt::Debug for OpenCodeClientCredential {
67    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68        formatter.write_str("OpenCodeClientCredential([REDACTED])")
69    }
70}
71
72impl Drop for OpenCodeClientCredential {
73    fn drop(&mut self) {
74        self.password.zeroize();
75    }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum OpenCodeEndpointHealth {
80    Ready,
81    ShuttingDown,
82    Stopped,
83}
84
85/// Removable HTTP endpoint around one already-authorized SDK runtime client.
86pub struct OpenCodeEndpoint {
87    adapter: Arc<OpenCodeAdapter>,
88    credential_digest: [u8; 32],
89}
90
91impl OpenCodeEndpoint {
92    pub fn new(
93        runtime: Arc<dyn SdkRuntime>,
94        runtime_id: impl Into<String>,
95        workspace: impl Into<PathBuf>,
96    ) -> Result<(Arc<Self>, OpenCodeClientCredential), CredentialError> {
97        let mut password = [0_u8; CREDENTIAL_BYTES];
98        getrandom::getrandom(&mut password).map_err(|_| CredentialError::RandomSource)?;
99        let mut basic = format!("opencode:{}", encode_hex(&password));
100        let credential_digest = *blake3::hash(basic.as_bytes()).as_bytes();
101        basic.zeroize();
102        Ok((
103            Arc::new(Self {
104                adapter: OpenCodeAdapter::new(runtime, runtime_id, workspace),
105                credential_digest,
106            }),
107            OpenCodeClientCredential { password },
108        ))
109    }
110
111    pub fn session_id(&self) -> &str {
112        self.adapter.session_id()
113    }
114
115    /// Bind only numeric IPv4 loopback. Remote use is an operator-owned
116    /// authenticated tunnel; there is no wildcard/arbitrary-address API.
117    pub async fn bind(self: &Arc<Self>, port: u16) -> std::io::Result<OpenCodeServerHandle> {
118        let listener =
119            TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)).await?;
120        let address = listener.local_addr()?;
121        let (shutdown, shutdown_receiver) = watch::channel(false);
122        let shutting_down = Arc::new(AtomicBool::new(false));
123        let stopped = Arc::new(AtomicBool::new(false));
124        let task = tokio::spawn(run_accept_loop(
125            listener,
126            self.clone(),
127            address,
128            shutdown_receiver,
129            shutting_down.clone(),
130            stopped.clone(),
131        ));
132        Ok(OpenCodeServerHandle {
133            address,
134            shutdown,
135            shutting_down,
136            stopped,
137            task: Some(task),
138        })
139    }
140
141    fn authenticate(&self, authorization: &str) -> bool {
142        let Some(encoded) = authorization.strip_prefix("Basic ") else {
143            return false;
144        };
145        let Ok(mut decoded) = base64::engine::general_purpose::STANDARD.decode(encoded) else {
146            return false;
147        };
148        let digest = *blake3::hash(&decoded).as_bytes();
149        decoded.zeroize();
150        constant_time_eq(&digest, &self.credential_digest)
151    }
152}
153
154pub struct OpenCodeServerHandle {
155    address: SocketAddr,
156    shutdown: watch::Sender<bool>,
157    shutting_down: Arc<AtomicBool>,
158    stopped: Arc<AtomicBool>,
159    task: Option<tokio::task::JoinHandle<()>>,
160}
161
162impl OpenCodeServerHandle {
163    pub fn address(&self) -> SocketAddr {
164        self.address
165    }
166
167    pub fn url(&self) -> String {
168        format!("http://{}", self.address)
169    }
170
171    pub fn health(&self) -> OpenCodeEndpointHealth {
172        if self.stopped.load(Ordering::SeqCst) {
173            OpenCodeEndpointHealth::Stopped
174        } else if self.shutting_down.load(Ordering::SeqCst) {
175            OpenCodeEndpointHealth::ShuttingDown
176        } else {
177            OpenCodeEndpointHealth::Ready
178        }
179    }
180
181    pub async fn shutdown(mut self) {
182        self.shutting_down.store(true, Ordering::SeqCst);
183        self.shutdown.send_replace(true);
184        if let Some(task) = self.task.take() {
185            let _ = task.await;
186        }
187    }
188}
189
190impl Drop for OpenCodeServerHandle {
191    fn drop(&mut self) {
192        self.shutting_down.store(true, Ordering::SeqCst);
193        self.shutdown.send_replace(true);
194        if let Some(task) = self.task.take() {
195            task.abort();
196        }
197    }
198}
199
200async fn run_accept_loop(
201    listener: TcpListener,
202    endpoint: Arc<OpenCodeEndpoint>,
203    address: SocketAddr,
204    mut shutdown: watch::Receiver<bool>,
205    shutting_down: Arc<AtomicBool>,
206    stopped: Arc<AtomicBool>,
207) {
208    let budget = Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_CONNECTIONS));
209    let mut connections = JoinSet::new();
210    loop {
211        tokio::select! {
212            changed = shutdown.changed() => {
213                if changed.is_err() || *shutdown.borrow() {
214                    break;
215                }
216            }
217            _ = connections.join_next(), if !connections.is_empty() => {}
218            accepted = listener.accept() => match accepted {
219                Ok((stream, _)) => {
220                    let Ok(permit) = budget.clone().try_acquire_owned() else {
221                        drop(stream);
222                        continue;
223                    };
224                    let endpoint = endpoint.clone();
225                    let mut connection_shutdown = shutdown.clone();
226                    connections.spawn(async move {
227                        let _permit = permit;
228                        tokio::select! {
229                            _ = wait_for_shutdown(&mut connection_shutdown) => {}
230                            _ = serve_connection(stream, endpoint, address) => {}
231                        }
232                    });
233                }
234                Err(_) => break,
235            }
236        }
237    }
238    connections.shutdown().await;
239    shutting_down.store(true, Ordering::SeqCst);
240    stopped.store(true, Ordering::SeqCst);
241}
242
243async fn wait_for_shutdown(shutdown: &mut watch::Receiver<bool>) {
244    while !*shutdown.borrow() {
245        if shutdown.changed().await.is_err() {
246            break;
247        }
248    }
249}
250
251async fn serve_connection(
252    mut stream: TcpStream,
253    endpoint: Arc<OpenCodeEndpoint>,
254    expected_address: SocketAddr,
255) -> std::io::Result<()> {
256    let request = match read_request(&mut stream, expected_address).await {
257        Ok(request) => request,
258        Err(error) => return write_error(&mut stream, error.status, error.message).await,
259    };
260    if !endpoint.authenticate(&request.authorization) {
261        return write_error(&mut stream, 401, "authentication failed").await;
262    }
263    let body = if request.body.is_empty() {
264        Value::Null
265    } else {
266        match serde_json::from_slice(&request.body) {
267            Ok(body) => body,
268            Err(_) => return write_error(&mut stream, 400, "invalid JSON body").await,
269        }
270    };
271    // Runtime mutations outlive a frontend connection. If the endpoint is
272    // shut down while a model turn is running, dropping this connection task
273    // must not cancel or partially unwind the SDK-owned turn.
274    let adapter = endpoint.adapter.clone();
275    let operation = tokio::spawn(async move {
276        adapter
277            .handle(OpenCodeRequest {
278                method: request.method,
279                target: request.target,
280                body,
281            })
282            .await
283    });
284    let response = operation.await.map_err(std::io::Error::other)?;
285    match response.body {
286        ResponseBody::Json(body) => write_json(&mut stream, response.status, &body).await,
287        ResponseBody::EventStream(attachment) => {
288            write_event_stream(&mut stream, endpoint.adapter.clone(), *attachment).await
289        }
290    }
291}
292
293struct WireRequest {
294    method: String,
295    target: String,
296    authorization: String,
297    body: Vec<u8>,
298}
299
300struct WireError {
301    status: u16,
302    message: &'static str,
303}
304
305async fn read_request(
306    stream: &mut TcpStream,
307    expected_address: SocketAddr,
308) -> Result<WireRequest, WireError> {
309    read_request_before(
310        stream,
311        expected_address,
312        tokio::time::Instant::now() + READ_TIMEOUT,
313    )
314    .await
315}
316
317async fn read_request_before(
318    stream: &mut TcpStream,
319    expected_address: SocketAddr,
320    deadline: tokio::time::Instant,
321) -> Result<WireRequest, WireError> {
322    let mut bytes = Vec::with_capacity(4096);
323    let header_end = loop {
324        if bytes.len() >= MAX_HEADER_BYTES {
325            return Err(WireError {
326                status: 431,
327                message: "request headers too large",
328            });
329        }
330        let mut chunk = [0_u8; 4096];
331        let read = tokio::time::timeout_at(deadline, stream.read(&mut chunk))
332            .await
333            .map_err(|_| WireError {
334                status: 408,
335                message: "request read timeout",
336            })?
337            .map_err(|_| WireError {
338                status: 400,
339                message: "request read failed",
340            })?;
341        if read == 0 {
342            return Err(WireError {
343                status: 400,
344                message: "incomplete request",
345            });
346        }
347        bytes.extend_from_slice(&chunk[..read]);
348        if let Some(index) = find_subslice(&bytes, b"\r\n\r\n") {
349            if index + 4 > MAX_HEADER_BYTES {
350                return Err(WireError {
351                    status: 431,
352                    message: "request headers too large",
353                });
354            }
355            break index + 4;
356        }
357    };
358    let header = std::str::from_utf8(&bytes[..header_end]).map_err(|_| WireError {
359        status: 400,
360        message: "request headers are not UTF-8",
361    })?;
362    let mut lines = header[..header.len() - 4].split("\r\n");
363    let request_line = lines.next().ok_or(WireError {
364        status: 400,
365        message: "missing request line",
366    })?;
367    let fields = request_line.split(' ').collect::<Vec<_>>();
368    if fields.len() != 3 || fields[2] != "HTTP/1.1" {
369        return Err(WireError {
370            status: 400,
371            message: "invalid HTTP/1.1 request line",
372        });
373    }
374    if !fields[1].starts_with('/') || fields[1].starts_with("//") {
375        return Err(WireError {
376            status: 400,
377            message: "absolute-form target rejected",
378        });
379    }
380    let method = fields[0].to_string();
381    let target = fields[1].to_string();
382    let mut host = None;
383    let mut authorization = None;
384    let mut content_length = None;
385    for line in lines {
386        let (name, value) = line.split_once(':').ok_or(WireError {
387            status: 400,
388            message: "invalid request header",
389        })?;
390        let name = name.to_ascii_lowercase();
391        let value = value.trim();
392        match name.as_str() {
393            "host" if host.replace(value).is_some() => return Err(duplicate_security_header()),
394            "authorization" if authorization.replace(value).is_some() => {
395                return Err(duplicate_security_header())
396            }
397            "content-length" if content_length.replace(value).is_some() => {
398                return Err(duplicate_security_header())
399            }
400            "transfer-encoding" | "origin" => {
401                return Err(WireError {
402                    status: 400,
403                    message: "forbidden request header",
404                })
405            }
406            _ => {}
407        }
408    }
409    if host != Some(expected_address.to_string().as_str()) {
410        return Err(WireError {
411            status: 400,
412            message: "invalid loopback Host",
413        });
414    }
415    let authorization = authorization
416        .ok_or(WireError {
417            status: 401,
418            message: "authentication failed",
419        })?
420        .to_string();
421    let content_length = content_length
422        .unwrap_or("0")
423        .parse::<usize>()
424        .map_err(|_| WireError {
425            status: 400,
426            message: "invalid content length",
427        })?;
428    if content_length > MAX_BODY_BYTES {
429        return Err(WireError {
430            status: 413,
431            message: "request body too large",
432        });
433    }
434    while bytes.len() - header_end < content_length {
435        let remaining = content_length - (bytes.len() - header_end);
436        let mut chunk = vec![0_u8; remaining.min(4096)];
437        let read = tokio::time::timeout_at(deadline, stream.read(&mut chunk))
438            .await
439            .map_err(|_| WireError {
440                status: 408,
441                message: "request body timeout",
442            })?
443            .map_err(|_| WireError {
444                status: 400,
445                message: "request body read failed",
446            })?;
447        if read == 0 {
448            return Err(WireError {
449                status: 400,
450                message: "incomplete request body",
451            });
452        }
453        bytes.extend_from_slice(&chunk[..read]);
454    }
455    if bytes.len() - header_end != content_length {
456        return Err(WireError {
457            status: 400,
458            message: "pipelined request bytes rejected",
459        });
460    }
461    Ok(WireRequest {
462        method,
463        target,
464        authorization,
465        body: bytes[header_end..].to_vec(),
466    })
467}
468
469async fn write_json(stream: &mut TcpStream, status: u16, body: &Value) -> std::io::Result<()> {
470    let body = serde_json::to_vec(body).expect("JSON Value serializes");
471    let reason = status_reason(status);
472    let header = format!(
473        "HTTP/1.1 {status} {reason}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\nX-Content-Type-Options: nosniff\r\n\r\n",
474        body.len()
475    );
476    stream.write_all(header.as_bytes()).await?;
477    stream.write_all(&body).await?;
478    stream.shutdown().await
479}
480
481async fn write_error(
482    stream: &mut TcpStream,
483    status: u16,
484    message: &'static str,
485) -> std::io::Result<()> {
486    write_json(
487        stream,
488        status,
489        &serde_json::json!({"name": "transport_error", "message": message}),
490    )
491    .await
492}
493
494async fn write_event_stream(
495    stream: &mut TcpStream,
496    adapter: Arc<OpenCodeAdapter>,
497    mut attachment: supercode::FrontendAttachment,
498) -> std::io::Result<()> {
499    stream
500        .write_all(
501            b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\nConnection: close\r\nX-Accel-Buffering: no\r\n\r\n",
502        )
503        .await?;
504    for event in adapter.initial_events(&attachment) {
505        write_sse(stream, &event).await?;
506    }
507    let mut projection = adapter.event_projection(&attachment);
508    loop {
509        tokio::select! {
510            event = attachment.next_event() => match event {
511                Ok(event) => {
512                    for projected in projection.project(&event) {
513                        write_sse(stream, &projected).await?;
514                    }
515                }
516                Err(_) => break,
517            },
518            _ = tokio::time::sleep(HEARTBEAT_INTERVAL) => {
519                write_sse(stream, &serde_json::json!({"type": "server.heartbeat", "properties": {}})).await?;
520            }
521        }
522    }
523    stream.shutdown().await
524}
525
526async fn write_sse(stream: &mut TcpStream, event: &Value) -> std::io::Result<()> {
527    let encoded = serde_json::to_vec(event).expect("JSON Value serializes");
528    if encoded.len() > MAX_EVENT_BYTES {
529        return Err(std::io::Error::new(
530            std::io::ErrorKind::InvalidData,
531            "projected event exceeded byte limit",
532        ));
533    }
534    stream.write_all(b"data: ").await?;
535    stream.write_all(&encoded).await?;
536    stream.write_all(b"\n\n").await?;
537    stream.flush().await
538}
539
540fn duplicate_security_header() -> WireError {
541    WireError {
542        status: 400,
543        message: "duplicate security header",
544    }
545}
546
547fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
548    haystack
549        .windows(needle.len())
550        .position(|window| window == needle)
551}
552
553fn constant_time_eq(left: &[u8; 32], right: &[u8; 32]) -> bool {
554    left.iter()
555        .zip(right)
556        .fold(0_u8, |difference, (left, right)| {
557            difference | (left ^ right)
558        })
559        == 0
560}
561
562fn encode_hex(secret: &[u8; CREDENTIAL_BYTES]) -> String {
563    const HEX: &[u8; 16] = b"0123456789abcdef";
564    let mut encoded = String::with_capacity(CREDENTIAL_BYTES * 2);
565    for byte in secret {
566        encoded.push(HEX[(byte >> 4) as usize] as char);
567        encoded.push(HEX[(byte & 0x0f) as usize] as char);
568    }
569    encoded
570}
571
572fn status_reason(status: u16) -> &'static str {
573    match status {
574        200 => "OK",
575        400 => "Bad Request",
576        401 => "Unauthorized",
577        403 => "Forbidden",
578        404 => "Not Found",
579        408 => "Request Timeout",
580        409 => "Conflict",
581        413 => "Payload Too Large",
582        431 => "Request Header Fields Too Large",
583        500 => "Internal Server Error",
584        _ => "Error",
585    }
586}
587
588#[cfg(test)]
589mod tests {
590    use std::collections::{BTreeMap, VecDeque};
591    use std::sync::atomic::{AtomicUsize, Ordering};
592    use std::sync::Mutex;
593
594    use async_trait::async_trait;
595    use supercode::{
596        ChatMessage, CoordinatedRuntime, FrontendActions, FrontendAttachSnapshot,
597        FrontendAttachment, FrontendConnectionState, FrontendDisplayCapabilities, FrontendResponse,
598        FrontendRuntimeDescriptor, FrontendTurnState, RuntimeAuthorization, RuntimeClientId,
599        SdkError, SdkEvent,
600    };
601    use tokio::sync::broadcast;
602
603    use super::*;
604
605    struct FixtureRuntime {
606        history: Mutex<Vec<ChatMessage>>,
607        submissions: AtomicUsize,
608        responses: Mutex<Vec<FrontendResponse>>,
609        events: broadcast::Sender<SdkEvent>,
610    }
611
612    impl FixtureRuntime {
613        fn new() -> Arc<Self> {
614            let (events, _) = broadcast::channel(64);
615            Arc::new(Self {
616                history: Mutex::new(vec![
617                    ChatMessage::system("Claude context"),
618                    ChatMessage::user("before"),
619                    ChatMessage::assistant("ready"),
620                ]),
621                submissions: AtomicUsize::new(0),
622                responses: Mutex::new(Vec::new()),
623                events,
624            })
625        }
626
627        fn descriptor() -> FrontendRuntimeDescriptor {
628            FrontendRuntimeDescriptor {
629                schema_version: 2,
630                session_id: "runtime-1".into(),
631                source_harness: Some("claude-code".into()),
632                emulation_profile: Some("claude-code".into()),
633                active_modules: Vec::new(),
634                commands: Vec::new(),
635                operations: Vec::new(),
636                actions: FrontendActions {
637                    submit: true,
638                    interrupt: true,
639                    steer: true,
640                    respond: true,
641                    detach: true,
642                    close: false,
643                },
644                display: FrontendDisplayCapabilities {
645                    event_kinds: vec!["text_delta".into(), "request".into()],
646                    opaque_fallback: true,
647                },
648                model: "openrouter/glm-5.2".into(),
649                turn_state: FrontendTurnState::Idle,
650                connection_state: FrontendConnectionState::Connected,
651                extensions: BTreeMap::new(),
652            }
653        }
654    }
655
656    #[async_trait]
657    impl SdkRuntime for FixtureRuntime {
658        async fn describe(&self) -> Result<FrontendRuntimeDescriptor, SdkError> {
659            Ok(Self::descriptor())
660        }
661
662        async fn attach(&self, history_limit: usize) -> Result<FrontendAttachment, SdkError> {
663            let history = self
664                .history
665                .lock()
666                .unwrap_or_else(std::sync::PoisonError::into_inner)
667                .clone();
668            let start = history.len().saturating_sub(history_limit);
669            Ok(FrontendAttachment::from_snapshot(
670                FrontendAttachSnapshot {
671                    descriptor: Self::descriptor(),
672                    history: history[start..].to_vec(),
673                    history_cursor: history.len() as u64,
674                    replay: VecDeque::new(),
675                },
676                self.events.subscribe(),
677            ))
678        }
679
680        async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), SdkError> {
681            self.submit(prompt).await.map(|_| ())
682        }
683
684        async fn submit(&self, prompt: String) -> Result<String, SdkError> {
685            self.submissions.fetch_add(1, Ordering::SeqCst);
686            let reply = format!("reply:{prompt}");
687            self.history
688                .lock()
689                .unwrap_or_else(std::sync::PoisonError::into_inner)
690                .extend([
691                    ChatMessage::user(prompt),
692                    ChatMessage::assistant(reply.clone()),
693                ]);
694            Ok(reply)
695        }
696
697        async fn interrupt(&self) -> Result<bool, SdkError> {
698            Ok(true)
699        }
700
701        async fn steer(&self, _prompt: String) -> Result<(), SdkError> {
702            Ok(())
703        }
704
705        async fn respond(&self, response: FrontendResponse) -> Result<(), SdkError> {
706            self.responses
707                .lock()
708                .unwrap_or_else(std::sync::PoisonError::into_inner)
709                .push(response);
710            Ok(())
711        }
712    }
713
714    async fn endpoint_for(
715        runtime: Arc<dyn SdkRuntime>,
716    ) -> (OpenCodeServerHandle, OpenCodeClientCredential) {
717        let (endpoint, credential) =
718            OpenCodeEndpoint::new(runtime, "runtime-1", "/runtime").expect("create endpoint");
719        let handle = endpoint.bind(0).await.expect("bind endpoint");
720        (handle, credential)
721    }
722
723    async fn raw_request(
724        address: SocketAddr,
725        authorization: Option<&str>,
726        method: &str,
727        target: &str,
728        body: &str,
729    ) -> String {
730        let mut stream = TcpStream::connect(address).await.unwrap();
731        let auth = authorization
732            .map(|authorization| format!("Authorization: {authorization}\r\n"))
733            .unwrap_or_default();
734        let request = format!(
735            "{method} {target} HTTP/1.1\r\nHost: {address}\r\n{auth}Content-Length: {}\r\nConnection: close\r\n\r\n{body}",
736            body.len()
737        );
738        stream.write_all(request.as_bytes()).await.unwrap();
739        let mut response = Vec::new();
740        stream.read_to_end(&mut response).await.unwrap();
741        String::from_utf8(response).unwrap()
742    }
743
744    async fn raw_wire(address: SocketAddr, request: String) -> String {
745        let mut stream = TcpStream::connect(address).await.unwrap();
746        stream.write_all(request.as_bytes()).await.unwrap();
747        let mut response = Vec::new();
748        stream.read_to_end(&mut response).await.unwrap();
749        String::from_utf8(response).unwrap()
750    }
751
752    async fn read_until(stream: &mut TcpStream, needle: &str) -> String {
753        let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
754        let mut bytes = Vec::new();
755        loop {
756            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
757            assert!(
758                !remaining.is_zero(),
759                "did not receive {needle}: {}",
760                String::from_utf8_lossy(&bytes)
761            );
762            let mut chunk = [0_u8; 4096];
763            let read = tokio::time::timeout(remaining, stream.read(&mut chunk))
764                .await
765                .expect("stream read deadline")
766                .expect("stream read");
767            assert!(read > 0, "stream closed before {needle}");
768            bytes.extend_from_slice(&chunk[..read]);
769            let text = String::from_utf8_lossy(&bytes);
770            if text.contains(needle) {
771                return text.into_owned();
772            }
773        }
774    }
775
776    fn message_body(prompt: &str) -> String {
777        serde_json::json!({
778            "messageID": "msg_stock", "agent": "build",
779            "model": {"providerID": "openrouter", "modelID": "glm-5.2"},
780            "parts": [{"id": "prt_stock", "type": "text", "text": prompt}]
781        })
782        .to_string()
783    }
784
785    #[tokio::test]
786    async fn loopback_host_requires_exact_basic_credential_and_host() {
787        let runtime = FixtureRuntime::new();
788        let (handle, credential) = endpoint_for(runtime).await;
789        let missing = raw_request(handle.address(), None, "GET", "/agent", "").await;
790        assert!(missing.starts_with("HTTP/1.1 401"));
791        let wrong = raw_request(
792            handle.address(),
793            Some("Basic b3BlbmNvZGU6d3Jvbmc="),
794            "GET",
795            "/agent",
796            "",
797        )
798        .await;
799        assert!(wrong.starts_with("HTTP/1.1 401"));
800        let authorized = raw_request(
801            handle.address(),
802            Some(&credential.authorization_header()),
803            "GET",
804            "/agent",
805            "",
806        )
807        .await;
808        assert!(authorized.starts_with("HTTP/1.1 200"));
809        assert!(!format!("{credential:?}").contains(&credential.authorization_header()));
810        handle.shutdown().await;
811    }
812
813    #[tokio::test]
814    async fn authenticated_owner_submits_but_observer_cannot_duplicate_work() {
815        let runtime = FixtureRuntime::new();
816        let coordinator = CoordinatedRuntime::new(runtime.clone());
817        let owner = coordinator.client(
818            RuntimeClientId::parse("opencode-owner").unwrap(),
819            RuntimeAuthorization::interactive(),
820        );
821        let (owner_handle, owner_credential) = endpoint_for(owner).await;
822        let session = OpenCodeAdapter::new(runtime.clone(), "runtime-1", "/runtime")
823            .session_id()
824            .to_string();
825        let response = raw_request(
826            owner_handle.address(),
827            Some(&owner_credential.authorization_header()),
828            "POST",
829            &format!("/session/{session}/message"),
830            &message_body("continue once"),
831        )
832        .await;
833        assert!(response.starts_with("HTTP/1.1 200"), "{response}");
834        assert_eq!(runtime.submissions.load(Ordering::SeqCst), 1);
835        owner_handle.shutdown().await;
836
837        let observer = coordinator.client(
838            RuntimeClientId::parse("opencode-observer").unwrap(),
839            RuntimeAuthorization::observer(),
840        );
841        let (observer_handle, observer_credential) = endpoint_for(observer).await;
842        let denied = raw_request(
843            observer_handle.address(),
844            Some(&observer_credential.authorization_header()),
845            "POST",
846            &format!("/session/{session}/message"),
847            &message_body("must not run"),
848        )
849        .await;
850        assert!(denied.starts_with("HTTP/1.1 409"), "{denied}");
851        assert_eq!(runtime.submissions.load(Ordering::SeqCst), 1);
852        observer_handle.shutdown().await;
853    }
854
855    #[tokio::test]
856    async fn transport_rejects_duplicate_security_headers_wrong_host_and_oversized_body() {
857        let runtime = FixtureRuntime::new();
858        let (handle, credential) = endpoint_for(runtime).await;
859        let auth = credential.authorization_header();
860        let duplicate = raw_wire(
861            handle.address(),
862            format!(
863                "GET /agent HTTP/1.1\r\nHost: {}\r\nAuthorization: {auth}\r\nAuthorization: {auth}\r\n\r\n",
864                handle.address()
865            ),
866        )
867        .await;
868        assert!(duplicate.starts_with("HTTP/1.1 400"));
869        let wrong_host = raw_wire(
870            handle.address(),
871            format!(
872                "GET /agent HTTP/1.1\r\nHost: localhost:{}\r\nAuthorization: {auth}\r\n\r\n",
873                handle.address().port()
874            ),
875        )
876        .await;
877        assert!(wrong_host.starts_with("HTTP/1.1 400"));
878        let oversized = raw_wire(
879            handle.address(),
880            format!(
881                "POST /session HTTP/1.1\r\nHost: {}\r\nAuthorization: {auth}\r\nContent-Length: {}\r\n\r\n",
882                handle.address(),
883                MAX_BODY_BYTES + 1
884            ),
885        )
886        .await;
887        assert!(oversized.starts_with("HTTP/1.1 413"));
888        handle.shutdown().await;
889    }
890
891    #[cfg(unix)]
892    #[tokio::test]
893    async fn credential_enters_only_one_child_environment_and_leaves_reusable_command_clean() {
894        use std::process::Stdio;
895
896        let runtime = FixtureRuntime::new();
897        let (_endpoint, credential) =
898            OpenCodeEndpoint::new(runtime, "runtime-1", "/runtime").unwrap();
899        let mut command = tokio::process::Command::new("sh");
900        command
901            .args([
902                "-c",
903                "printf '%s:%s' \"$OPENCODE_SERVER_USERNAME\" \"$OPENCODE_SERVER_PASSWORD\"",
904            ])
905            .stdout(Stdio::piped());
906        let output = credential
907            .spawn_tokio_child(&mut command)
908            .unwrap()
909            .wait_with_output()
910            .await
911            .unwrap();
912        let first = String::from_utf8(output.stdout).unwrap();
913        assert!(first.starts_with("opencode:"));
914        assert_eq!(first.len(), "opencode:".len() + CREDENTIAL_BYTES * 2);
915
916        let reused = command.output().await.unwrap();
917        assert_eq!(reused.stdout, b":");
918    }
919
920    #[tokio::test]
921    async fn event_stream_projects_live_sdk_delta_and_permission_without_second_runtime() {
922        let runtime = FixtureRuntime::new();
923        let (handle, credential) = endpoint_for(runtime.clone()).await;
924        let mut stream = TcpStream::connect(handle.address()).await.unwrap();
925        let request = format!(
926            "GET /event HTTP/1.1\r\nHost: {}\r\nAuthorization: {}\r\nContent-Length: 0\r\n\r\n",
927            handle.address(),
928            credential.authorization_header()
929        );
930        stream.write_all(request.as_bytes()).await.unwrap();
931        let initial = read_until(&mut stream, "session.status").await;
932        assert!(initial.contains("text/event-stream"));
933
934        runtime
935            .events
936            .send(SdkEvent {
937                sequence: 10,
938                kind: "text_delta".into(),
939                payload: serde_json::json!({"type": "text_delta", "text": "live GLM output"}),
940            })
941            .unwrap();
942        let projected = read_until(&mut stream, "live GLM output").await;
943        assert!(projected.contains("message.part.delta"), "{projected}");
944        assert!(projected.contains("live GLM output"), "{projected}");
945        drop(stream);
946        handle.shutdown().await;
947    }
948
949    #[tokio::test]
950    async fn shutdown_is_durable_and_closes_an_active_event_stream() {
951        for _ in 0..16 {
952            let (handle, _credential) = endpoint_for(FixtureRuntime::new()).await;
953            tokio::time::timeout(Duration::from_secs(1), handle.shutdown())
954                .await
955                .expect("shutdown sent immediately after bind must not be lost");
956        }
957
958        let (handle, credential) = endpoint_for(FixtureRuntime::new()).await;
959        let mut stream = TcpStream::connect(handle.address()).await.unwrap();
960        let request = format!(
961            "GET /event HTTP/1.1\r\nHost: {}\r\nAuthorization: {}\r\nContent-Length: 0\r\n\r\n",
962            handle.address(),
963            credential.authorization_header()
964        );
965        stream.write_all(request.as_bytes()).await.unwrap();
966        let initial = read_until(&mut stream, "session.updated").await;
967        assert!(initial.contains("text/event-stream"));
968        tokio::time::timeout(Duration::from_secs(1), handle.shutdown())
969            .await
970            .expect("active SSE connection must be joined during shutdown");
971        let mut tail = Vec::new();
972        tokio::time::timeout(Duration::from_secs(1), stream.read_to_end(&mut tail))
973            .await
974            .expect("SSE peer must observe endpoint shutdown")
975            .expect("read closed SSE stream");
976    }
977
978    #[tokio::test]
979    async fn request_deadline_is_total_not_reset_by_trickled_bytes() {
980        let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
981        let address = listener.local_addr().unwrap();
982        let mut client = TcpStream::connect(address).await.unwrap();
983        let (mut server, _) = listener.accept().await.unwrap();
984        let writer = tokio::spawn(async move {
985            for byte in b"GET /agent HTTP/1.1\r\n" {
986                if client.write_all(&[*byte]).await.is_err() {
987                    break;
988                }
989                tokio::time::sleep(Duration::from_millis(10)).await;
990            }
991        });
992        let started = tokio::time::Instant::now();
993        let error =
994            match read_request_before(&mut server, address, started + Duration::from_millis(35))
995                .await
996            {
997                Ok(_) => panic!("trickled request must exceed one total deadline"),
998                Err(error) => error,
999            };
1000        assert_eq!(error.status, 408);
1001        assert!(started.elapsed() < Duration::from_millis(100));
1002        writer.abort();
1003    }
1004}