Skip to main content

supercode_harness/
server.rs

1//! §2 module 31 `server` (COMPOSABLE-HARNESS-DESIGN.md, D7 "full
2//! programmatic RPC/HTTP server", D8 "remote attach", D10 "daemon"; §1.9
3//! Obligation 9's out-of-process half — the in-process SDK already meets
4//! the core commitment via [`crate::EventSink`]).
5//!
6//! The embedding ladder this module builds:
7//!
8//! 1. **`--output-format stream-json`** (the CLI's existing rung, UX-23) —
9//!    already ships a JSONL [`crate::AgentEvent`] stream over stdout. This
10//!    unit completes it: [`crate::AgentEvent::to_json`] is now the single
11//!    canonical projection both that sink AND this module's notifications
12//!    share, and it covers the FULL event set (previously
13//!    [`crate::AgentEvent::BackgroundOutput`] fell into a generic
14//!    "unknown" catch-all).
15//! 2. **JSONL-RPC over stdio** ([`run_stdio`]) — the SDK-out-of-process
16//!    surface: a parent process drives this agent's loop over stdin/stdout
17//!    with `{"id","method","params"}` request lines, getting back
18//!    `{"id","result"|"error"}` responses interleaved with
19//!    `{"event":...}` notifications. Parent-process-trusted (same trust
20//!    model as [`crate::mcp::serve_stdio`]) — no auth token.
21//! 3. **The same RPC surface over HTTP** ([`run_http`], D8 "remote
22//!    attach") — `POST /rpc` for request/response, `GET /events` for the
23//!    event stream (SSE-shaped: `data: <json>\n\n` per line). Unlike
24//!    stdio, a network client is UNTRUSTED by default, so every request
25//!    must carry the bearer token (`check_auth`).
26//!
27//! **Security posture (this is a listener — the highest-risk module
28//! class):**
29//! - `[capabilities.server]` is project-forbidden (D-10) — see
30//!   `crates/cli/src/userconfig.rs`'s `PROJECT_FORBIDDEN_CAPABILITY_TABLES`
31//!   and this crate's `configfile::PROJECT_FORBIDDEN_CAPABILITY_TABLES`
32//!   (both already listed `"server"` before this unit landed; this module
33//!   is what makes the listener the strip was already guarding against
34//!   real).
35//! - Default-off: nothing in this module is ever reached unless a caller
36//!   explicitly invokes [`run_stdio`]/[`run_http`] AND the CLI's own
37//!   gate (`capabilities.server.enabled == Some(true)`, checked before
38//!   either is called) passed.
39//! - Loopback-only HTTP bind by default — enforced by the CALLER (the
40//!   CLI's `serve` command defaults `bind` to `127.0.0.1:0` and only binds
41//!   elsewhere on an explicit `bind`/`--bind` override, with a printed
42//!   exposure warning); [`run_http`] itself binds whatever address it's
43//!   given, since the loopback POLICY decision belongs to the config/CLI
44//!   layer, not the transport.
45//! - **No permission/sandbox bypass.** [`RpcEngine::new`] takes an already
46//!   fully-constructed [`crate::Agent`] — the SAME `Agent` a local
47//!   `run`/`chat` session would build (same `Config`, same permission
48//!   rules, same sandbox). This module installs NO approval handler of its
49//!   own and provides no channel for a remote/RPC caller to answer an
50//!   approval prompt; combined with `Agent`'s existing fail-closed rule
51//!   ("absent handler denies" — `crates/harness/src/agent.rs`'s
52//!   `prepare_tool_call`), any tool call that would need interactive
53//!   approval is DENIED, never silently approved, when driven through this
54//!   module. See `crates/harness/tests/server_engine.rs` for a fail-on-revert
55//!   proof.
56//! - Bounded buffering throughout ([`SERVER_MAX_LINE_BYTES`],
57//!   [`SERVER_EVENT_CHANNEL_CAPACITY`]) — same 16MiB-class discipline P5-2
58//!   established for `crate::mcp`'s SSE reader, reused here rather than
59//!   re-derived.
60//! - Graceful shutdown: the `shutdown` RPC method stops the stdio loop and
61//!   the HTTP accept loop alike (both select on the same
62//!   [`RpcEngine::wait_for_shutdown`]) — no orphaned listener/accept task
63//!   survives a `shutdown` call, mirroring P5-3/P5-6's drop-abort
64//!   discipline for background work.
65
66use std::collections::{BTreeMap, HashMap, VecDeque};
67#[cfg(feature = "adapter-api")]
68use std::net::SocketAddr;
69use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
70use std::sync::{Arc, Mutex as StdMutex};
71
72use async_trait::async_trait;
73#[cfg(feature = "adapter-api")]
74use futures::{SinkExt, StreamExt};
75use serde_json::{json, Value};
76use tokio::io::AsyncBufRead;
77#[cfg(feature = "adapter-api")]
78use tokio::io::{AsyncRead, AsyncReadExt};
79#[cfg(feature = "adapter-api")]
80use tokio::io::{AsyncWrite, AsyncWriteExt};
81#[cfg(feature = "adapter-api")]
82use tokio::net::TcpListener;
83#[cfg(feature = "adapter-api")]
84use tokio::sync::mpsc;
85use tokio::sync::{broadcast, Mutex, Notify, RwLock};
86
87use crate::agent::SteerInbox;
88use crate::frontend::{
89    FrontendActions, FrontendApprovalDecision, FrontendAttachSnapshot, FrontendAttachment,
90    FrontendCommandDescriptor, FrontendConnectionState, FrontendDisplayCapabilities, FrontendEvent,
91    FrontendOperationDescriptor, FrontendOperationInvocation, FrontendOperationKind,
92    FrontendOperationResult, FrontendProjectionState, FrontendRequest, FrontendRequestKind,
93    FrontendResponse, FrontendRuntime, FrontendRuntimeDescriptor, FrontendRuntimeError,
94    FrontendRuntimeMetadata, FrontendTurnState, FRONTEND_EVENT_SCHEMA_VERSION,
95    FRONTEND_REPLAY_CAPACITY, FRONTEND_RUNTIME_SCHEMA_VERSION,
96};
97use crate::mcp::{
98    ElicitationAction, ElicitationRequest, ElicitationResponse, McpElicitationHandler,
99};
100use crate::message::ChatMessage;
101use crate::permissions::{ApprovalOutcome, ApprovalRequest, PermissionsApprovalHandler};
102pub use crate::sdk::RuntimeSubmitError;
103use crate::sdk::SdkAgent;
104#[cfg(feature = "adapter-api")]
105use crate::{CoordinatedRuntime, CoordinatedRuntimeClient, RuntimeAuthorization, RuntimeClientId};
106
107/// Bounded broadcast capacity for the event-notification channel — mirrors
108/// `crate::mcp::MCP_SSE_CHANNEL_CAPACITY`'s bounded-buffering discipline
109/// (P5-2): a slow/absent subscriber can never make the sender block or
110/// grow memory unboundedly; a lagging receiver just misses old events
111/// (`broadcast::error::RecvError::Lagged`) rather than stalling the agent
112/// loop or accumulating unbounded backlog.
113pub const SERVER_EVENT_CHANNEL_CAPACITY: usize = 1024;
114
115/// Maximum canonical messages retained for late-attaching frontends. This
116/// matches the `history` RPC limit and prevents the lock-independent snapshot
117/// from duplicating an arbitrarily large agent transcript.
118pub(crate) const SERVER_HISTORY_CAPACITY: usize = 200;
119
120/// Maximum accepted line/body length (bytes) for both the stdio JSONL-RPC
121/// reader and the HTTP transport's request line/headers/body — the same
122/// 16MiB-class cap P5-2 established for `crate::mcp`'s SSE frame reader
123/// (`MCP_MAX_SSE_FRAME_BYTES`), reused here so an adversarial or simply
124/// broken client can never make either transport buffer an unbounded
125/// amount of data in memory.
126pub const SERVER_MAX_LINE_BYTES: usize = 16 * 1024 * 1024;
127
128/// Cap on HTTP header line COUNT (independent of [`SERVER_MAX_LINE_BYTES`],
129/// which only bounds any one line's length) — without this, a client could
130/// send an unbounded NUMBER of small, individually-under-cap header lines
131/// and still exhaust memory over one connection.
132#[cfg(feature = "adapter-api")]
133const MAX_HEADER_LINES: usize = 200;
134
135/// One JSONL-RPC request line a client sends: `{"id", "method", "params"}`.
136/// `params` defaults to `null` when omitted (a method that takes no
137/// arguments, e.g. `status`/`shutdown`, never requires callers to spell out
138/// `"params": null}` explicitly).
139#[derive(Debug, Clone, serde::Deserialize)]
140pub struct RpcRequest {
141    /// Caller-chosen correlation id, echoed back verbatim on the matching
142    /// response — never interpreted, so any JSON value (string, number,
143    /// null) a caller likes works.
144    pub id: Value,
145    /// The method name (`submit` | `interrupt` | `status` | `shutdown`).
146    pub method: String,
147    /// Method-specific arguments; `submit` reads `params.prompt`.
148    #[serde(default)]
149    pub params: Value,
150}
151
152/// Build a `{"id", "result"}` response line.
153fn rpc_ok(id: Value, result: Value) -> Value {
154    json!({"id": id, "result": result})
155}
156
157/// Build a `{"id", "error": {"code","message"}}` response line. `code`
158/// follows JSON-RPC 2.0's reserved-range convention where a natural fit
159/// exists (`-32700` parse error, `-32601` method not found, `-32602`
160/// invalid params) purely as a familiar, self-documenting convention — this
161/// protocol does not otherwise claim JSON-RPC 2.0 compliance (no
162/// `"jsonrpc":"2.0"` envelope; see the module doc's minimal wire shape).
163fn rpc_error(id: Value, code: i32, message: impl Into<String>) -> Value {
164    json!({"id": id, "error": {"code": code, "message": message.into()}})
165}
166
167fn sdk_runtime_rpc_error(id: Value, code: i32, error: &FrontendRuntimeError) -> Value {
168    let code = match error.code() {
169        crate::SdkErrorCode::Unauthenticated => -32030,
170        crate::SdkErrorCode::Unauthorized => -32031,
171        crate::SdkErrorCode::ControllerRequired => -32032,
172        crate::SdkErrorCode::LeaseExpired => -32033,
173        _ => code,
174    };
175    let mut envelope = json!({
176        "id": id,
177        "error": {
178            "code": code,
179            "name": error.code(),
180            "operation": error.operation(),
181            "message": error.to_string(),
182        }
183    });
184    if let Some(detail) = envelope.get_mut("error").and_then(Value::as_object_mut) {
185        match error {
186            FrontendRuntimeError::Unauthorized { permission } => {
187                detail.insert("permission".into(), Value::String(permission.clone()));
188            }
189            FrontendRuntimeError::ControllerRequired {
190                holder,
191                expires_at_ms,
192            } => {
193                if let Some(holder) = holder {
194                    detail.insert("holder".into(), Value::String(holder.clone()));
195                }
196                if let Some(expires_at_ms) = expires_at_ms {
197                    detail.insert("expiresAtMs".into(), json!(expires_at_ms));
198                }
199            }
200            _ => {}
201        }
202    }
203    envelope
204}
205
206/// Read one line (trailing `\n`/`\r\n` stripped) from `reader`, capped at
207/// `cap` bytes — mirrors `crate::mcp`'s `SseLineAccumulator` bounded-
208/// buffering discipline (P5-2). Returns `Ok(None)` at a clean EOF with no
209/// partial line pending. On an over-cap line, the REST of that oversized
210/// line is drained and discarded (up to the next `\n`) so the stream
211/// resyncs at the next real line boundary instead of desyncing forever,
212/// and `Err` is returned naming the cap.
213async fn read_bounded_line<R>(reader: &mut R, cap: usize) -> std::io::Result<Option<String>>
214where
215    R: AsyncBufRead + Unpin,
216{
217    use tokio::io::AsyncBufReadExt;
218    let mut out: Vec<u8> = Vec::new();
219    loop {
220        let buf = reader.fill_buf().await?;
221        if buf.is_empty() {
222            return Ok(if out.is_empty() {
223                None
224            } else {
225                Some(strip_crlf(out))
226            });
227        }
228        if let Some(pos) = buf.iter().position(|&b| b == b'\n') {
229            if out.len() + pos > cap {
230                reader.consume(pos + 1);
231                return Err(std::io::Error::new(
232                    std::io::ErrorKind::InvalidData,
233                    format!("line exceeded {cap} byte cap"),
234                ));
235            }
236            out.extend_from_slice(&buf[..pos]);
237            reader.consume(pos + 1);
238            return Ok(Some(strip_crlf(out)));
239        }
240        let take = buf.len();
241        if out.len() + take > cap {
242            reader.consume(take);
243            // Drain/discard the rest of this oversized line so a future
244            // read starts at the next real line boundary.
245            loop {
246                let b = reader.fill_buf().await?;
247                if b.is_empty() {
248                    break;
249                }
250                if let Some(p) = b.iter().position(|&x| x == b'\n') {
251                    reader.consume(p + 1);
252                    break;
253                }
254                let n = b.len();
255                reader.consume(n);
256            }
257            return Err(std::io::Error::new(
258                std::io::ErrorKind::InvalidData,
259                format!("line exceeded {cap} byte cap"),
260            ));
261        }
262        out.extend_from_slice(buf);
263        reader.consume(take);
264    }
265}
266
267fn strip_crlf(mut v: Vec<u8>) -> String {
268    if v.last() == Some(&b'\r') {
269        v.pop();
270    }
271    String::from_utf8_lossy(&v).into_owned()
272}
273
274/// Constant-time byte-slice comparison (avoids leaking the bearer token
275/// through a timing side-channel on `==`) — small, self-contained, no new
276/// dependency for one comparison.
277#[cfg(feature = "adapter-api")]
278fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
279    if a.len() != b.len() {
280        return false;
281    }
282    let mut diff = 0u8;
283    for (x, y) in a.iter().zip(b.iter()) {
284        diff |= x ^ y;
285    }
286    diff == 0
287}
288
289/// Mint a random per-session bearer token (32 bytes, hex-encoded) for the
290/// HTTP transport, when the operator hasn't configured a fixed
291/// `capabilities.server.token`. Uses `getrandom` (already resolved
292/// transitively via `reqwest`'s rustls/ring stack; promoted to a direct
293/// dependency here so this crate can call it directly, rather than rolling
294/// a hand-written PRNG for a value that must actually be unguessable).
295pub fn generate_token() -> String {
296    let mut bytes = [0u8; 32];
297    // `getrandom::getrandom` only fails if the OS entropy source itself is
298    // unavailable/misconfigured — effectively never on a real target this
299    // crate supports. Falling back to a process-time/PID-derived value
300    // would be a WORSE, easily-guessable token, so a failure here is
301    // treated as fatal (panic) rather than silently minting a weak secret.
302    getrandom::getrandom(&mut bytes).expect("OS entropy source for the server bearer token");
303    bytes.iter().map(|b| format!("{b:02x}")).collect()
304}
305
306/// The `on_turn_complete` hook's type — factored out (clippy
307/// `type_complexity`) rather than spelled out at both
308/// [`RpcEngine`]'s field and [`RpcEngine::new`]'s parameter.
309type TurnCompleteHook = Box<dyn Fn(&SdkAgent) + Send + Sync>;
310
311/// Protocol-neutral snapshot of one SDK-owned agent runtime.
312///
313/// Transport adapters (CLI/RPC/HTTP/ACP) project this value into their own
314/// wire shapes instead of each inventing a separate definition of "busy".
315#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
316pub struct RuntimeStatus {
317    /// Stable identity chosen by the embedding surface/session store.
318    pub session_id: String,
319    /// Model currently backing the agent.
320    pub model: String,
321    /// Whether a user or scheduler turn currently owns the agent loop.
322    pub busy: bool,
323    /// Whether graceful shutdown has been requested.
324    pub shutting_down: bool,
325}
326
327type PendingFrontendResponses = StdMutex<
328    HashMap<
329        u64,
330        (
331            FrontendRequestKind,
332            std::sync::mpsc::Sender<AcceptedFrontendResponse>,
333        ),
334    >,
335>;
336
337struct AcceptedFrontendResponse {
338    response: FrontendResponse,
339    /// The blocked handler may resume execution only after the canonical
340    /// resolution event has entered the sequenced frontend projection.
341    published: std::sync::mpsc::Receiver<()>,
342}
343
344/// Runtime-owned interactive request broker. It publishes complete request
345/// payloads into the same sequenced event stream and resolves each id once.
346struct FrontendRequestBroker {
347    next_id: std::sync::atomic::AtomicU64,
348    pending: PendingFrontendResponses,
349    transport: StdMutex<Option<FrontendRequestTransport>>,
350}
351
352#[derive(Clone)]
353struct FrontendRequestTransport {
354    events: broadcast::Sender<FrontendEvent>,
355    state: Arc<StdMutex<FrontendProjectionState>>,
356}
357
358impl FrontendRequestBroker {
359    fn new() -> Arc<Self> {
360        Arc::new(Self {
361            next_id: std::sync::atomic::AtomicU64::new(1),
362            pending: StdMutex::new(HashMap::new()),
363            transport: StdMutex::new(None),
364        })
365    }
366
367    fn bind(
368        &self,
369        events: broadcast::Sender<FrontendEvent>,
370        state: Arc<StdMutex<FrontendProjectionState>>,
371    ) {
372        *self
373            .transport
374            .lock()
375            .unwrap_or_else(std::sync::PoisonError::into_inner) =
376            Some(FrontendRequestTransport { events, state });
377    }
378
379    fn transport(&self) -> Option<FrontendRequestTransport> {
380        self.transport
381            .lock()
382            .unwrap_or_else(std::sync::PoisonError::into_inner)
383            .clone()
384    }
385
386    fn publish(&self, request: &FrontendRequest) -> bool {
387        self.publish_payload(json!({"type": "request", "request": request}))
388    }
389
390    fn publish_payload(&self, payload: Value) -> bool {
391        let Some(transport) = self.transport() else {
392            return false;
393        };
394        let event = {
395            let mut state = transport
396                .state
397                .lock()
398                .unwrap_or_else(std::sync::PoisonError::into_inner);
399            let event = FrontendEvent::new(state.next_sequence, payload);
400            state.next_sequence = state.next_sequence.saturating_add(1);
401            state.replay.push_back(event.clone());
402            while state.replay.len() > FRONTEND_REPLAY_CAPACITY {
403                state.replay.pop_front();
404            }
405            event
406        };
407        transport.events.send(event).is_ok()
408    }
409
410    fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
411        let request_id = response.request_id();
412        let response_kind = match &response {
413            FrontendResponse::Approval { .. } => FrontendRequestKind::Approval,
414            FrontendResponse::Elicitation { .. } => FrontendRequestKind::Elicitation,
415            FrontendResponse::Other { .. } => FrontendRequestKind::Other,
416        };
417        let mut pending = self
418            .pending
419            .lock()
420            .unwrap_or_else(std::sync::PoisonError::into_inner);
421        let expected = pending
422            .get(&request_id)
423            .map(|(kind, _)| *kind)
424            .ok_or(FrontendRuntimeError::UnknownRequest(request_id))?;
425        if expected != response_kind {
426            return Err(FrontendRuntimeError::InvalidResponse(format!(
427                "request {request_id} expects {expected:?}, got {response_kind:?}"
428            )));
429        }
430        let (_, sender) = pending
431            .remove(&request_id)
432            .ok_or(FrontendRuntimeError::UnknownRequest(request_id))?;
433        drop(pending);
434        let payload = json!({
435            "type": "request_resolved",
436            "request_id": request_id,
437            "response": &response,
438        });
439        let (published_tx, published_rx) = std::sync::mpsc::channel();
440        sender
441            .send(AcceptedFrontendResponse {
442                response,
443                published: published_rx,
444            })
445            .map_err(|_| FrontendRuntimeError::UnknownRequest(request_id))?;
446        self.publish_payload(payload);
447        let _ = published_tx.send(());
448        Ok(())
449    }
450
451    fn ask_approval(
452        &self,
453        req: &ApprovalRequest<'_>,
454        child: Option<(&str, &Arc<StdMutex<Vec<crate::subagents::QueuedApproval>>>)>,
455    ) -> ApprovalOutcome {
456        // Record with no outcome first: while this call is in flight the
457        // entry IS a pending request, and the decision is written back onto
458        // the same record below so a reader never has to guess.
459        let queued = child.and_then(|(child_agent_id, queue)| {
460            crate::subagents::queue_approval(
461                queue,
462                crate::subagents::QueuedApproval {
463                    child_agent_id: child_agent_id.to_string(),
464                    tool: req.tool.to_string(),
465                    subject: req.subject.map(String::from),
466                    queued_at_ms: std::time::SystemTime::now()
467                        .duration_since(std::time::UNIX_EPOCH)
468                        .map(|duration| duration.as_millis() as i64)
469                        .unwrap_or_default(),
470                    outcome: None,
471                },
472            )
473            .map(|index| (queue.clone(), index))
474        });
475        let outcome = self.decide_approval(req, child.map(|(id, _)| id));
476        if let Some((queue, index)) = queued {
477            crate::subagents::record_queued_outcome(&queue, index, outcome.into());
478        }
479        outcome
480    }
481
482    fn decide_approval(
483        &self,
484        req: &ApprovalRequest<'_>,
485        child_agent_id: Option<&str>,
486    ) -> ApprovalOutcome {
487        // No observer means no human can answer. Preserve the server's
488        // existing fail-closed, non-blocking headless behavior.
489        let Some(transport) = self.transport() else {
490            return ApprovalOutcome::Deny;
491        };
492        if transport.events.receiver_count() == 0 {
493            return ApprovalOutcome::Deny;
494        }
495        let id = self.next_id.fetch_add(1, Ordering::SeqCst);
496        let mut payload = json!({
497            "tool": req.tool,
498            "subject": req.subject,
499            "raw_args": req.raw_args,
500        });
501        if let Some(child_agent_id) = child_agent_id {
502            payload["child_agent_id"] = Value::String(child_agent_id.to_string());
503        }
504        let request = FrontendRequest {
505            id,
506            kind: FrontendRequestKind::Approval,
507            payload,
508        };
509        let (tx, rx) = std::sync::mpsc::channel();
510        self.pending
511            .lock()
512            .unwrap_or_else(std::sync::PoisonError::into_inner)
513            .insert(id, (FrontendRequestKind::Approval, tx));
514        if !self.publish(&request) {
515            self.pending
516                .lock()
517                .unwrap_or_else(std::sync::PoisonError::into_inner)
518                .remove(&id);
519            return ApprovalOutcome::Deny;
520        }
521        let wait_for_response = || loop {
522            match rx.recv_timeout(std::time::Duration::from_millis(100)) {
523                Ok(accepted) => {
524                    let _ = accepted.published.recv();
525                    let FrontendResponse::Approval { decision, .. } = accepted.response else {
526                        return ApprovalOutcome::Deny;
527                    };
528                    return match decision {
529                        FrontendApprovalDecision::Deny => ApprovalOutcome::Deny,
530                        FrontendApprovalDecision::Allow => ApprovalOutcome::Allow,
531                        FrontendApprovalDecision::AllowForSession => {
532                            ApprovalOutcome::AllowForSession
533                        }
534                    };
535                }
536                Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
537                    return ApprovalOutcome::Deny;
538                }
539                Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
540                    if self
541                        .transport()
542                        .map(|transport| transport.events.receiver_count() == 0)
543                        .unwrap_or(true)
544                    {
545                        self.pending
546                            .lock()
547                            .unwrap_or_else(std::sync::PoisonError::into_inner)
548                            .remove(&id);
549                        return ApprovalOutcome::Deny;
550                    }
551                }
552            }
553        };
554        if tokio::runtime::Handle::try_current()
555            .map(|handle| handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread)
556            .unwrap_or(false)
557        {
558            tokio::task::block_in_place(wait_for_response)
559        } else {
560            wait_for_response()
561        }
562    }
563
564    async fn ask_elicitation(self: Arc<Self>, req: &ElicitationRequest) -> ElicitationResponse {
565        let cancel = || ElicitationResponse {
566            action: ElicitationAction::Cancel,
567            content: None,
568        };
569        let Some(transport) = self.transport() else {
570            return cancel();
571        };
572        if transport.events.receiver_count() == 0 {
573            return cancel();
574        }
575        let id = self.next_id.fetch_add(1, Ordering::SeqCst);
576        let request = FrontendRequest {
577            id,
578            kind: FrontendRequestKind::Elicitation,
579            payload: json!({
580                "message": req.message,
581                "requested_schema": req.requested_schema,
582            }),
583        };
584        let (tx, rx) = std::sync::mpsc::channel();
585        self.pending
586            .lock()
587            .unwrap_or_else(std::sync::PoisonError::into_inner)
588            .insert(id, (FrontendRequestKind::Elicitation, tx));
589        if !self.publish(&request) {
590            self.pending
591                .lock()
592                .unwrap_or_else(std::sync::PoisonError::into_inner)
593                .remove(&id);
594            return cancel();
595        }
596        let broker = self.clone();
597        tokio::task::spawn_blocking(move || loop {
598            match rx.recv_timeout(std::time::Duration::from_millis(100)) {
599                Ok(accepted) => {
600                    let _ = accepted.published.recv();
601                    let FrontendResponse::Elicitation {
602                        action, content, ..
603                    } = accepted.response
604                    else {
605                        return cancel();
606                    };
607                    return ElicitationResponse {
608                        action: match action {
609                            crate::frontend::FrontendElicitationAction::Accept => {
610                                ElicitationAction::Accept
611                            }
612                            crate::frontend::FrontendElicitationAction::Decline => {
613                                ElicitationAction::Decline
614                            }
615                            crate::frontend::FrontendElicitationAction::Cancel => {
616                                ElicitationAction::Cancel
617                            }
618                        },
619                        content,
620                    };
621                }
622                Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return cancel(),
623                Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
624                    if broker
625                        .transport()
626                        .map(|transport| transport.events.receiver_count() == 0)
627                        .unwrap_or(true)
628                    {
629                        broker
630                            .pending
631                            .lock()
632                            .unwrap_or_else(std::sync::PoisonError::into_inner)
633                            .remove(&id);
634                        return cancel();
635                    }
636                }
637            }
638        })
639        .await
640        .unwrap_or_else(|_| cancel())
641    }
642}
643
644struct FrontendApprovalHandler(Arc<FrontendRequestBroker>);
645
646impl PermissionsApprovalHandler for FrontendApprovalHandler {
647    fn ask(&self, req: &ApprovalRequest<'_>) -> ApprovalOutcome {
648        self.0.ask_approval(req, None)
649    }
650}
651
652struct FrontendChildApprovalHandler {
653    broker: Arc<FrontendRequestBroker>,
654    child_agent_id: String,
655    queue: Arc<StdMutex<Vec<crate::subagents::QueuedApproval>>>,
656}
657
658impl PermissionsApprovalHandler for FrontendChildApprovalHandler {
659    fn ask(&self, req: &ApprovalRequest<'_>) -> ApprovalOutcome {
660        self.broker
661            .ask_approval(req, Some((&self.child_agent_id, &self.queue)))
662    }
663}
664
665/// Pre-runtime bridge for MCP clients that must receive their elicitation
666/// handler before they are consumed into agent tool registration.
667#[derive(Clone)]
668pub struct FrontendRequestBridge {
669    broker: Arc<FrontendRequestBroker>,
670}
671
672impl FrontendRequestBridge {
673    /// Create an unbound bridge. Pass it to
674    /// [`RpcEngine::new_named_with_frontend_bridge`] after MCP registration.
675    pub fn new() -> Self {
676        Self {
677            broker: FrontendRequestBroker::new(),
678        }
679    }
680
681    /// Handler installed on every interactive MCP client before registration.
682    pub fn elicitation_handler(&self) -> Arc<dyn McpElicitationHandler> {
683        Arc::new(FrontendElicitationHandler(self.broker.clone()))
684    }
685}
686
687impl Default for FrontendRequestBridge {
688    fn default() -> Self {
689        Self::new()
690    }
691}
692
693struct FrontendElicitationHandler(Arc<FrontendRequestBroker>);
694
695#[async_trait]
696impl McpElicitationHandler for FrontendElicitationHandler {
697    async fn handle(&self, request: &ElicitationRequest) -> ElicitationResponse {
698        self.0.clone().ask_elicitation(request).await
699    }
700}
701
702/// The out-of-process RPC driver: wraps one already-constructed
703/// [`crate::Agent`] with the `submit`/`interrupt`/`status`/`shutdown`
704/// method set (§ module doc). Shared by both transports ([`run_stdio`],
705/// [`run_http`]) so the method semantics — including the fail-closed
706/// permission behavior — can never drift between them.
707pub struct RpcEngine {
708    agent: Mutex<SdkAgent>,
709    /// Last canonical transcript observed at a turn boundary. Frontends must
710    /// be able to attach and replay prior history while `submit` holds the
711    /// agent lock for an active turn, so reads use this independent snapshot.
712    history_snapshot: RwLock<Vec<ChatMessage>>,
713    session_id: String,
714    /// The model label, captured once at construction so `status` never
715    /// needs to lock `agent` (which `submit` holds for the WHOLE turn) —
716    /// `status` must stay answerable while a turn is in flight.
717    model: String,
718    busy: Arc<AtomicBool>,
719    /// The in-flight turn's cancellation handle, if any — see
720    /// [`Self::handle_submit`]/[`Self::handle_interrupt`]'s doc comments
721    /// for why this is a fresh [`Notify`] per turn rather than one shared
722    /// instance (`notify_one`'s stored-permit semantics only give the
723    /// correctness guarantee this needs when each turn gets a clean one).
724    current_cancel: Arc<StdMutex<Option<Arc<Notify>>>>,
725    /// Signals that the terminal event for an interrupted/completed turn is
726    /// published and its canonical history snapshot is stable.
727    turn_finished: Arc<Notify>,
728    /// Shared control queue owned by `Agent` but writable without waiting for
729    /// the active turn's long-held async agent lock.
730    steer_queue: Arc<StdMutex<SteerInbox>>,
731    events: broadcast::Sender<Value>,
732    /// Sequenced frontend events used for atomic replay/live attachment.
733    frontend_events: broadcast::Sender<FrontendEvent>,
734    /// Short-held projection lock. It is never held across model/tool I/O.
735    frontend_state: Arc<StdMutex<FrontendProjectionState>>,
736    frontend_metadata: FrontendRuntimeMetadata,
737    frontend_active_modules: Vec<String>,
738    frontend_commands: Vec<FrontendCommandDescriptor>,
739    frontend_operations: Vec<FrontendOperationDescriptor>,
740    frontend_requests: Option<Arc<FrontendRequestBroker>>,
741    shutdown: Notify,
742    shutting_down: AtomicBool,
743    /// Explicit owner shutdown seals new model-loop claims before it
744    /// interrupts the active one. Plain transport EOF only raises
745    /// `shutting_down` so already-buffered stdio requests can still flush.
746    accepting_submits: AtomicBool,
747    /// Serializes explicit shutdown barriers so every concurrent caller
748    /// returns only after the same admitted turn.
749    shutdown_barrier: Mutex<()>,
750    /// Fires after every SUCCESSFUL `submit` (never on an errored/
751    /// interrupted turn — see the call site), with the agent still locked
752    /// so the hook sees fully-consistent state (e.g. `agent.history()`).
753    /// The CLI installs session persistence/auto-titling here — this
754    /// module itself has no opinion on session storage.
755    on_turn_complete: Option<TurnCompleteHook>,
756}
757
758/// Owns every externally visible piece of an SDK submit claim from the
759/// instant the claim succeeds until the submit reaches a terminal boundary.
760/// The agent loop closes the ordinary final-answer steering boundary
761/// atomically with its last drain; this outer guard also restores steering,
762/// cancellation, busy state, and lifecycle waiters when the public submit
763/// future is dropped or fails before `Agent::run_loop` is entered.
764struct SdkSubmitClaim {
765    inbox: Arc<StdMutex<SteerInbox>>,
766    busy: Arc<AtomicBool>,
767    cancel: Arc<Notify>,
768    current_cancel: Arc<StdMutex<Option<Arc<Notify>>>>,
769    turn_finished: Arc<Notify>,
770    frontend_events: broadcast::Sender<FrontendEvent>,
771    frontend_state: Arc<StdMutex<FrontendProjectionState>>,
772    lifecycle_started: bool,
773}
774
775impl SdkSubmitClaim {
776    fn mark_lifecycle_started(&mut self) {
777        self.lifecycle_started = true;
778    }
779
780    fn mark_lifecycle_finished(&mut self) {
781        self.lifecycle_started = false;
782    }
783}
784
785impl Drop for SdkSubmitClaim {
786    fn drop(&mut self) {
787        // A caller may cancel the public `submit` future after the SDK has
788        // exposed `turn_started` but before `submit_claimed` can publish its
789        // ordinary terminal event. Close that exact lifecycle before making
790        // the runtime idle so replay and live frontends cannot remain busy on
791        // an abandoned turn. There is no await between the normal terminal
792        // publication and disarming this fallback, so exactly one terminal
793        // event is observable for every started claim.
794        if self.lifecycle_started {
795            let event = {
796                let mut state = self
797                    .frontend_state
798                    .lock()
799                    .unwrap_or_else(std::sync::PoisonError::into_inner);
800                let event = FrontendEvent::new(
801                    state.next_sequence,
802                    json!({
803                        "type": "turn_interrupted",
804                        "schema_version": FRONTEND_EVENT_SCHEMA_VERSION
805                    }),
806                );
807                state.next_sequence = state.next_sequence.saturating_add(1);
808                state.replay.push_back(event.clone());
809                while state.replay.len() > FRONTEND_REPLAY_CAPACITY {
810                    state.replay.pop_front();
811                }
812                event
813            };
814            let _ = self.frontend_events.send(event);
815        }
816        self.inbox
817            .lock()
818            .unwrap_or_else(std::sync::PoisonError::into_inner)
819            .close();
820        *self
821            .current_cancel
822            .lock()
823            .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
824        self.busy.store(false, Ordering::SeqCst);
825        self.turn_finished.notify_waiters();
826        self.turn_finished.notify_one();
827    }
828}
829
830impl RpcEngine {
831    /// Wrap `agent` (already fully built by the caller — same `Config`,
832    /// same permission/sandbox posture as a local session) for out-of-
833    /// process driving. Installs its OWN event sink via
834    /// `Agent::set_event_sink`, overwriting whatever sink `agent` may
835    /// already have had wired (callers of this module drive an agent
836    /// exclusively through the RPC surface, so there is never a second,
837    /// competing consumer of its events).
838    pub fn new(
839        agent: impl Into<SdkAgent>,
840        on_turn_complete: Option<TurnCompleteHook>,
841    ) -> Arc<Self> {
842        let agent = agent.into();
843        let session_id = agent
844            .session_name()
845            .map(str::to_owned)
846            .unwrap_or_else(|| format!("supercode-{}", std::process::id()));
847        Self::new_named(agent, session_id, on_turn_complete)
848    }
849
850    /// Construct the canonical SDK runtime with an explicit durable session
851    /// identity.  Every frontend must use this identity when referring to the
852    /// same live agent; transport-local connection ids are not session ids.
853    pub fn new_named(
854        agent: impl Into<SdkAgent>,
855        session_id: impl Into<String>,
856        on_turn_complete: Option<TurnCompleteHook>,
857    ) -> Arc<Self> {
858        Self::new_named_with_frontend_metadata(
859            agent.into(),
860            session_id,
861            FrontendRuntimeMetadata::default(),
862            on_turn_complete,
863        )
864    }
865
866    /// Construct the canonical runtime with explicit source-harness and
867    /// emulation-profile identity for every attached frontend.
868    pub fn new_named_with_frontend_metadata(
869        agent: impl Into<SdkAgent>,
870        session_id: impl Into<String>,
871        frontend_metadata: FrontendRuntimeMetadata,
872        on_turn_complete: Option<TurnCompleteHook>,
873    ) -> Arc<Self> {
874        Self::build(
875            agent.into(),
876            session_id.into(),
877            frontend_metadata,
878            None,
879            on_turn_complete,
880        )
881    }
882
883    /// Construct a canonical runtime whose attached frontends may answer
884    /// policy-authorized approval requests. Existing constructors retain the
885    /// historical fail-closed headless behavior and report `respond=false`.
886    pub fn new_named_with_frontend_requests(
887        agent: impl Into<SdkAgent>,
888        session_id: impl Into<String>,
889        frontend_metadata: FrontendRuntimeMetadata,
890        on_turn_complete: Option<TurnCompleteHook>,
891    ) -> Arc<Self> {
892        let bridge = FrontendRequestBridge::new();
893        Self::new_named_with_frontend_bridge(
894            agent.into(),
895            session_id,
896            frontend_metadata,
897            bridge,
898            on_turn_complete,
899        )
900    }
901
902    /// Bind a pre-created request bridge after its elicitation handler has
903    /// been installed on MCP clients.
904    pub fn new_named_with_frontend_bridge(
905        agent: impl Into<SdkAgent>,
906        session_id: impl Into<String>,
907        frontend_metadata: FrontendRuntimeMetadata,
908        bridge: FrontendRequestBridge,
909        on_turn_complete: Option<TurnCompleteHook>,
910    ) -> Arc<Self> {
911        Self::build(
912            agent.into(),
913            session_id.into(),
914            frontend_metadata,
915            Some(bridge.broker),
916            on_turn_complete,
917        )
918    }
919
920    fn build(
921        mut agent: SdkAgent,
922        session_id: String,
923        frontend_metadata: FrontendRuntimeMetadata,
924        frontend_requests: Option<Arc<FrontendRequestBroker>>,
925        on_turn_complete: Option<TurnCompleteHook>,
926    ) -> Arc<Self> {
927        let (tx, _rx) = broadcast::channel(SERVER_EVENT_CHANNEL_CAPACITY);
928        let events_tx = tx.clone();
929        let (frontend_tx, _frontend_rx) = broadcast::channel(SERVER_EVENT_CHANNEL_CAPACITY);
930        let frontend_events_tx = frontend_tx.clone();
931        let model = agent.config().model.clone();
932        let steer_queue = agent.inner().steer_queue_handle();
933        let history_snapshot = bounded_history_snapshot(agent.history());
934        let frontend_state = Arc::new(StdMutex::new(FrontendProjectionState {
935            history: history_snapshot.clone(),
936            history_cursor: 0,
937            next_sequence: 1,
938            replay: VecDeque::new(),
939        }));
940        if let Some(broker) = &frontend_requests {
941            broker.bind(frontend_tx.clone(), frontend_state.clone());
942            let legacy_broker = broker.clone();
943            agent
944                .inner_mut()
945                .set_legacy_approval_handler(Box::new(move |call| {
946                    let Ok(raw_args) = call.function.parsed_arguments() else {
947                        return false;
948                    };
949                    let subject = raw_args
950                        .get("command")
951                        .or_else(|| raw_args.get("path"))
952                        .or_else(|| raw_args.get("file_path"))
953                        .or_else(|| raw_args.get("patch"))
954                        .and_then(Value::as_str);
955                    matches!(
956                        legacy_broker.ask_approval(
957                            &ApprovalRequest {
958                                tool: &call.function.name,
959                                subject,
960                                raw_args: &raw_args,
961                            },
962                            None,
963                        ),
964                        ApprovalOutcome::Allow | ApprovalOutcome::AllowForSession
965                    )
966                }));
967            agent
968                .inner_mut()
969                .set_permissions_approval_handler(FrontendApprovalHandler(broker.clone()));
970            // BP-3 (§2 module 6 `tools.question`): `ask_user` asks through
971            // the SAME broker — the request is published into the sequenced
972            // frontend event stream and the turn blocks on it until
973            // `respond` answers, exactly like the approval above. Without
974            // this the tool would be deny-default even on an attached
975            // frontend.
976            agent
977                .inner_mut()
978                .set_user_question_handler(Arc::new(FrontendElicitationHandler(broker.clone())));
979            let broker = broker.clone();
980            agent
981                .inner_mut()
982                .set_child_approval_handler_factory(move |child_agent_id, queue| {
983                    Arc::new(FrontendChildApprovalHandler {
984                        broker: broker.clone(),
985                        child_agent_id,
986                        queue,
987                    }) as Arc<dyn PermissionsApprovalHandler>
988                });
989        }
990        let event_frontend_state = frontend_state.clone();
991        let frontend_active_modules = agent
992            .config()
993            .module_activation
994            .iter()
995            .map(ToString::to_string)
996            .collect();
997        let mut frontend_operations = agent
998            .config()
999            .prompts
1000            .keys()
1001            .filter(|name| valid_frontend_command_name(name))
1002            .map(|name| FrontendOperationDescriptor {
1003                id: format!("prompt:{name}"),
1004                kind: FrontendOperationKind::Prompt,
1005                command: Some(FrontendCommandDescriptor {
1006                    name: name.clone(),
1007                    description: None,
1008                    argument_hint: Some("[arguments]".into()),
1009                }),
1010            })
1011            .collect::<Vec<_>>();
1012        frontend_operations.sort_by(|left, right| left.id.cmp(&right.id));
1013        // BP-4 (catalog:109): the one operation every runtime can always
1014        // answer — it reads the accounting it already computes for the
1015        // context guard, submits nothing, and mutates nothing. Advertised
1016        // unconditionally for that reason (it is not inferred from a module
1017        // set, which is what the "phantom operation" gate forbids), and
1018        // pushed AFTER the id sort so the prompt catalog's own ordering is
1019        // untouched.
1020        // BP-13 (catalog D9 "Mid-session model switching"): the model
1021        // control, advertised only when this runtime's config actually
1022        // allows a switch — the same "never advertise a phantom operation"
1023        // rule the context row above obeys, read from the one gate
1024        // (`[core.model_switch] allow_switch`) rather than inferred.
1025        if agent.config().model_switch_allow_switch {
1026            frontend_operations.push(FrontendOperationDescriptor {
1027                id: "model:switch".into(),
1028                kind: FrontendOperationKind::Model,
1029                command: Some(FrontendCommandDescriptor {
1030                    name: "model".into(),
1031                    description: Some("show or switch this session's model".into()),
1032                    argument_hint: Some("[model]".into()),
1033                }),
1034            });
1035        }
1036        frontend_operations.push(FrontendOperationDescriptor {
1037            id: "context:usage".into(),
1038            kind: FrontendOperationKind::Context,
1039            command: Some(FrontendCommandDescriptor {
1040                name: "context".into(),
1041                description: Some("context-window usage for this session".into()),
1042                argument_hint: None,
1043            }),
1044        });
1045        // Schema-v1 compatibility projection. New frontends use only the
1046        // typed operation catalog and never fall back to this list.
1047        let frontend_commands = frontend_operations
1048            .iter()
1049            .filter_map(|operation| operation.command.as_ref())
1050            .map(|command| FrontendCommandDescriptor {
1051                name: command.name.clone(),
1052                description: command.description.clone(),
1053                argument_hint: None,
1054            })
1055            .collect();
1056        agent.inner_mut().set_event_sink(Box::new(move |event| {
1057            // A `send` error here only means "no subscriber is currently
1058            // listening" (every receiver dropped) — never a reason to fail
1059            // the turn itself, so it's intentionally discarded.
1060            let payload = event.to_json();
1061            let _ = events_tx.send(payload.clone());
1062            let sequenced = {
1063                let mut state = event_frontend_state
1064                    .lock()
1065                    .unwrap_or_else(std::sync::PoisonError::into_inner);
1066                let event = FrontendEvent::new(state.next_sequence, payload);
1067                state.next_sequence = state.next_sequence.saturating_add(1);
1068                state.replay.push_back(event.clone());
1069                while state.replay.len() > FRONTEND_REPLAY_CAPACITY {
1070                    state.replay.pop_front();
1071                }
1072                event
1073            };
1074            let _ = frontend_events_tx.send(sequenced);
1075        }));
1076        Arc::new(RpcEngine {
1077            agent: Mutex::new(agent),
1078            history_snapshot: RwLock::new(history_snapshot),
1079            session_id,
1080            model,
1081            busy: Arc::new(AtomicBool::new(false)),
1082            current_cancel: Arc::new(StdMutex::new(None)),
1083            turn_finished: Arc::new(Notify::new()),
1084            steer_queue,
1085            events: tx,
1086            frontend_events: frontend_tx,
1087            frontend_state,
1088            frontend_metadata,
1089            frontend_active_modules,
1090            frontend_commands,
1091            frontend_operations,
1092            frontend_requests,
1093            shutdown: Notify::new(),
1094            shutting_down: AtomicBool::new(false),
1095            accepting_submits: AtomicBool::new(true),
1096            shutdown_barrier: Mutex::new(()),
1097            on_turn_complete,
1098        })
1099    }
1100
1101    /// Subscribe to this engine's event-notification stream (already
1102    /// `AgentEvent::to_json`-projected) — each subscriber gets every event
1103    /// emitted from this point on, independent of any other subscriber.
1104    pub fn subscribe(&self) -> broadcast::Receiver<Value> {
1105        self.events.subscribe()
1106    }
1107
1108    /// Describe the SDK-owned runtime without locking the active agent turn.
1109    pub fn frontend_descriptor(&self) -> FrontendRuntimeDescriptor {
1110        FrontendRuntimeDescriptor {
1111            schema_version: FRONTEND_RUNTIME_SCHEMA_VERSION,
1112            session_id: self.session_id.clone(),
1113            source_harness: self.frontend_metadata.source_harness.clone(),
1114            emulation_profile: self.frontend_metadata.emulation_profile.clone(),
1115            active_modules: self.frontend_active_modules.clone(),
1116            commands: self.frontend_commands.clone(),
1117            operations: self.frontend_operations.clone(),
1118            actions: FrontendActions {
1119                submit: true,
1120                interrupt: true,
1121                steer: true,
1122                respond: self.frontend_requests.is_some(),
1123                detach: true,
1124                // The canonical runtime supports close. Coordinated client
1125                // projections mask this unless the authenticated grant owns
1126                // the independent terminate capability.
1127                close: true,
1128            },
1129            display: FrontendDisplayCapabilities {
1130                event_kinds: vec![
1131                    "user_message".into(),
1132                    "turn_started".into(),
1133                    "turn_succeeded".into(),
1134                    "turn_interrupted".into(),
1135                    "turn_failed".into(),
1136                    "text_delta".into(),
1137                    "turn_completed".into(),
1138                    "tool_call_started".into(),
1139                    "tool_call_completed".into(),
1140                    "cache_warning".into(),
1141                    "usage".into(),
1142                    "background_output".into(),
1143                    "request".into(),
1144                    "request_resolved".into(),
1145                    "scheduled_prompt_started".into(),
1146                    "scheduled_prompt_deferred".into(),
1147                    "scheduled_prompt_completed".into(),
1148                    "scheduler_error".into(),
1149                ],
1150                opaque_fallback: true,
1151            },
1152            model: self.model.clone(),
1153            turn_state: if self.busy.load(Ordering::SeqCst) {
1154                FrontendTurnState::Busy
1155            } else {
1156                FrontendTurnState::Idle
1157            },
1158            connection_state: if self.is_shutting_down() {
1159                FrontendConnectionState::ShuttingDown
1160            } else {
1161                FrontendConnectionState::Connected
1162            },
1163            extensions: Default::default(),
1164        }
1165    }
1166
1167    /// Attach to one atomic history/replay/live boundary. The live receiver
1168    /// is created before the projection snapshot is locked; events racing the
1169    /// snapshot therefore appear either in replay or in the receiver, and
1170    /// [`FrontendAttachment::next_event`] removes any overlap by sequence.
1171    pub fn frontend_attach(
1172        &self,
1173        history_limit: usize,
1174    ) -> Result<FrontendAttachment, FrontendRuntimeError> {
1175        let live = self.frontend_subscribe();
1176        let snapshot = self.frontend_snapshot(history_limit)?;
1177        Ok(FrontendAttachment::new(
1178            snapshot.descriptor,
1179            snapshot.history,
1180            snapshot.history_cursor,
1181            snapshot.replay,
1182            live,
1183            None,
1184        ))
1185    }
1186
1187    /// Subscribe to sequenced frontend events. Transport adapters subscribe
1188    /// before taking [`Self::frontend_snapshot`] so boundary events cannot be
1189    /// missed.
1190    pub fn frontend_subscribe(&self) -> broadcast::Receiver<FrontendEvent> {
1191        self.frontend_events.subscribe()
1192    }
1193
1194    /// Capture the serializable history/replay half of a frontend attachment.
1195    pub fn frontend_snapshot(
1196        &self,
1197        history_limit: usize,
1198    ) -> Result<FrontendAttachSnapshot, FrontendRuntimeError> {
1199        let state = self
1200            .frontend_state
1201            .lock()
1202            .unwrap_or_else(std::sync::PoisonError::into_inner);
1203        let limit = history_limit.min(SERVER_HISTORY_CAPACITY);
1204        let start = state.history.len().saturating_sub(limit);
1205        let replay = state
1206            .replay
1207            .iter()
1208            .filter(|event| event.sequence > state.history_cursor)
1209            .cloned()
1210            .collect::<VecDeque<_>>();
1211        if let Some(first) = replay.front() {
1212            let expected = state.history_cursor.saturating_add(1);
1213            if first.sequence > expected {
1214                return Err(FrontendRuntimeError::ReplayGap(first.sequence - expected));
1215            }
1216        }
1217        Ok(FrontendAttachSnapshot {
1218            descriptor: self.frontend_descriptor(),
1219            history: state.history[start..].to_vec(),
1220            history_cursor: state.history_cursor,
1221            replay,
1222        })
1223    }
1224
1225    fn publish_frontend_payload(&self, payload: Value) {
1226        let event = {
1227            let mut state = self
1228                .frontend_state
1229                .lock()
1230                .unwrap_or_else(std::sync::PoisonError::into_inner);
1231            let event = FrontendEvent::new(state.next_sequence, payload);
1232            state.next_sequence = state.next_sequence.saturating_add(1);
1233            state.replay.push_back(event.clone());
1234            while state.replay.len() > FRONTEND_REPLAY_CAPACITY {
1235                state.replay.pop_front();
1236            }
1237            event
1238        };
1239        let _ = self.frontend_events.send(event);
1240    }
1241
1242    /// Stable SDK session identity shared by every frontend.
1243    pub fn session_id(&self) -> &str {
1244        &self.session_id
1245    }
1246
1247    fn claim_submit(&self) -> Result<SdkSubmitClaim, RuntimeSubmitError> {
1248        // Hold the cancellation slot across admission. Shutdown seals first,
1249        // then takes this same lock through `interrupt`: it therefore either
1250        // wins before `busy` is claimed or observes the admitted turn's
1251        // installed token. There is no busy-without-cancel interval.
1252        let mut current_cancel = self
1253            .current_cancel
1254            .lock()
1255            .unwrap_or_else(std::sync::PoisonError::into_inner);
1256        if !self.accepting_submits.load(Ordering::SeqCst) {
1257            return Err(RuntimeSubmitError::Interrupted);
1258        }
1259        if self.busy.swap(true, Ordering::SeqCst) {
1260            return Err(RuntimeSubmitError::Busy);
1261        }
1262        // Close the race with a concurrent shutdown between the first state
1263        // check and ownership of `busy`; relinquish this claim without
1264        // exposing a turn when the shutdown seal won.
1265        if !self.accepting_submits.load(Ordering::SeqCst) {
1266            self.busy.store(false, Ordering::SeqCst);
1267            self.turn_finished.notify_waiters();
1268            return Err(RuntimeSubmitError::Interrupted);
1269        }
1270        let cancel = Arc::new(Notify::new());
1271        *current_cancel = Some(cancel.clone());
1272        drop(current_cancel);
1273        self.steer_queue
1274            .lock()
1275            .unwrap_or_else(std::sync::PoisonError::into_inner)
1276            .open();
1277        Ok(SdkSubmitClaim {
1278            inbox: self.steer_queue.clone(),
1279            busy: self.busy.clone(),
1280            cancel,
1281            current_cancel: self.current_cancel.clone(),
1282            turn_finished: self.turn_finished.clone(),
1283            frontend_events: self.frontend_events.clone(),
1284            frontend_state: self.frontend_state.clone(),
1285            lifecycle_started: false,
1286        })
1287    }
1288
1289    async fn submit_claimed(
1290        &self,
1291        prompt: String,
1292        image_urls: Vec<String>,
1293        mut submit_claim: SdkSubmitClaim,
1294    ) -> Result<String, RuntimeSubmitError> {
1295        let cancel = submit_claim.cancel.clone();
1296        self.publish_frontend_payload(json!({"type": "user_message", "text": &prompt}));
1297        self.publish_frontend_payload(json!({
1298            "type": "turn_started",
1299            "schema_version": FRONTEND_EVENT_SCHEMA_VERSION
1300        }));
1301        submit_claim.mark_lifecycle_started();
1302        let outcome = {
1303            let mut agent = self.agent.lock().await;
1304            let result = tokio::select! {
1305                biased;
1306                _ = cancel.notified() => Err(RuntimeSubmitError::Interrupted),
1307                result = async {
1308                    if image_urls.is_empty() {
1309                        agent.inner_mut().send(&prompt).await
1310                    } else {
1311                        agent.inner_mut().send_with_images(&prompt, &image_urls).await
1312                    }
1313                } => result.map_err(|error| RuntimeSubmitError::Agent(error.to_string())),
1314            };
1315            if result.is_ok() {
1316                if let Some(hook) = &self.on_turn_complete {
1317                    hook(&agent);
1318                }
1319            }
1320            // `Agent::send` is cancellation-safe at await boundaries. Publish
1321            // its latest well-formed history on success, failure, or
1322            // interruption without making attach readers wait on `agent`.
1323            let history = bounded_history_snapshot(agent.history());
1324            *self.history_snapshot.write().await = history.clone();
1325            let mut state = self
1326                .frontend_state
1327                .lock()
1328                .unwrap_or_else(std::sync::PoisonError::into_inner);
1329            state.history = history;
1330            state.history_cursor = state.next_sequence.saturating_sub(1);
1331            // Canonical ChatMessage history represents user/model/tool
1332            // content, but not interactive frontend requests or the human's
1333            // typed decision. Re-sequence those semantic events immediately
1334            // after the history boundary so later attachments retain a
1335            // resolved transcript without replaying ordinary turn events
1336            // already represented by `history`.
1337            let request_history = compact_frontend_request_history(&state.replay);
1338            state.replay.clear();
1339            for payload in request_history {
1340                let event = FrontendEvent::new(state.next_sequence, payload);
1341                state.next_sequence = state.next_sequence.saturating_add(1);
1342                state.replay.push_back(event);
1343                while state.replay.len() > FRONTEND_REPLAY_CAPACITY {
1344                    state.replay.pop_front();
1345                }
1346            }
1347            result
1348        };
1349        *self
1350            .current_cancel
1351            .lock()
1352            .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
1353        let lifecycle = match &outcome {
1354            Ok(reply) => json!({
1355                "type": "turn_succeeded",
1356                "schema_version": FRONTEND_EVENT_SCHEMA_VERSION,
1357                "reply": reply,
1358            }),
1359            Err(RuntimeSubmitError::Interrupted) => json!({
1360                "type": "turn_interrupted",
1361                "schema_version": FRONTEND_EVENT_SCHEMA_VERSION
1362            }),
1363            Err(error) => json!({
1364                "type": "turn_failed",
1365                "schema_version": FRONTEND_EVENT_SCHEMA_VERSION,
1366                "message": error.to_string()
1367            }),
1368        };
1369        self.publish_frontend_payload(lifecycle);
1370        submit_claim.mark_lifecycle_finished();
1371        // Close before publishing idle. This is redundant with the agent's
1372        // normal final-boundary close, but is authoritative for pre-loop
1373        // validation/record failures and biased immediate interruption.
1374        drop(submit_claim);
1375        outcome
1376    }
1377
1378    /// Submit one prompt through the canonical runtime and wait for its reply.
1379    pub async fn submit(&self, prompt: impl Into<String>) -> Result<String, RuntimeSubmitError> {
1380        let submit_claim = self.claim_submit()?;
1381        self.submit_claimed(prompt.into(), Vec::new(), submit_claim)
1382            .await
1383    }
1384
1385    /// Submit one prompt with runtime-owned multimodal image inputs.
1386    pub async fn submit_with_images(
1387        &self,
1388        prompt: impl Into<String>,
1389        image_urls: Vec<String>,
1390    ) -> Result<String, RuntimeSubmitError> {
1391        let submit_claim = self.claim_submit()?;
1392        self.submit_claimed(prompt.into(), image_urls, submit_claim)
1393            .await
1394    }
1395
1396    /// Atomically claim one prompt, then run it on the SDK owner while the
1397    /// caller consumes the canonical event stream.
1398    pub fn send_input(self: &Arc<Self>, prompt: String) -> Result<(), RuntimeSubmitError> {
1399        self.send_input_with_images(prompt, Vec::new())
1400    }
1401
1402    /// Atomically claim one multimodal prompt, then run it while callers
1403    /// consume the canonical event stream.
1404    pub fn send_input_with_images(
1405        self: &Arc<Self>,
1406        prompt: String,
1407        image_urls: Vec<String>,
1408    ) -> Result<(), RuntimeSubmitError> {
1409        let submit_claim = self.claim_submit()?;
1410        let runtime = self.clone();
1411        tokio::spawn(async move {
1412            let _ = runtime
1413                .submit_claimed(prompt, image_urls, submit_claim)
1414                .await;
1415        });
1416        Ok(())
1417    }
1418
1419    /// Queue steering for the active agent loop without waiting for its
1420    /// long-held async lock. The agent consumes it at the next model-loop
1421    /// boundary according to the configured steering mode.
1422    pub fn steer(&self, prompt: impl Into<String>) -> Result<(), FrontendRuntimeError> {
1423        let accepted = self
1424            .steer_queue
1425            .lock()
1426            .unwrap_or_else(std::sync::PoisonError::into_inner)
1427            .enqueue(prompt.into());
1428        if accepted {
1429            Ok(())
1430        } else {
1431            Err(FrontendRuntimeError::UnsupportedAction("steer"))
1432        }
1433    }
1434
1435    /// Resolve one pending interactive request exactly once.
1436    pub fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
1437        self.frontend_requests
1438            .as_ref()
1439            .ok_or(FrontendRuntimeError::UnsupportedAction("respond"))?
1440            .respond(response)
1441    }
1442
1443    /// Invoke one operation after resolving its opaque identifier solely
1444    /// against the trusted catalog captured at runtime construction.
1445    pub async fn invoke(
1446        &self,
1447        operation: FrontendOperationInvocation,
1448    ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
1449        match operation {
1450            FrontendOperationInvocation::Prompt {
1451                operation_id,
1452                arguments,
1453            } => {
1454                let prompt_name = self
1455                    .frontend_operations
1456                    .iter()
1457                    .find(|descriptor| {
1458                        descriptor.id == operation_id
1459                            && descriptor.kind == FrontendOperationKind::Prompt
1460                    })
1461                    .and_then(|descriptor| descriptor.command.as_ref())
1462                    .map(|command| command.name.as_str())
1463                    .ok_or_else(|| {
1464                        FrontendRuntimeError::UnsupportedOperation(operation_id.clone())
1465                    })?;
1466                let prompt = if arguments.is_empty() {
1467                    format!("/{prompt_name}")
1468                } else {
1469                    format!("/{prompt_name} {arguments}")
1470                };
1471                let reply = self.submit(prompt).await?;
1472                Ok(FrontendOperationResult::Prompt { reply })
1473            }
1474            FrontendOperationInvocation::Context { operation_id } => {
1475                if !self.frontend_operations.iter().any(|descriptor| {
1476                    descriptor.id == operation_id
1477                        && descriptor.kind == FrontendOperationKind::Context
1478                }) {
1479                    return Err(FrontendRuntimeError::UnsupportedOperation(operation_id));
1480                }
1481                // Short-held read lock: `context_usage` is pure and never
1482                // touches the provider, so this can never be held across
1483                // model I/O.
1484                let usage = self.agent.lock().await.context_usage();
1485                Ok(FrontendOperationResult::Context { usage })
1486            }
1487            FrontendOperationInvocation::Model {
1488                operation_id,
1489                model,
1490            } => {
1491                if !self.frontend_operations.iter().any(|descriptor| {
1492                    descriptor.id == operation_id && descriptor.kind == FrontendOperationKind::Model
1493                }) {
1494                    return Err(FrontendRuntimeError::UnsupportedOperation(operation_id));
1495                }
1496                let mut agent = self.agent.lock().await;
1497                let previous = agent.model().to_string();
1498                if model.trim().is_empty() {
1499                    return Ok(FrontendOperationResult::Model {
1500                        model: previous.clone(),
1501                        previous,
1502                    });
1503                }
1504                // BP-13: the ONE resolution path — the same routing table
1505                // `--model` and the request build read, so a friendly name
1506                // typed here means exactly what it means everywhere else.
1507                let resolved = agent
1508                    .config()
1509                    .model_routing
1510                    .resolve_alias(model.trim())
1511                    .to_string();
1512                if let Some(refusal) = agent.config().model_routing.refusal(&resolved) {
1513                    return Err(FrontendRuntimeError::UnsupportedOperation(refusal));
1514                }
1515                // `switch_model`, never `set_model`: the governed switch
1516                // filters model-A reasoning artifacts out of the live
1517                // history (dep 8) and records the change in the session
1518                // journal before model B ever builds a request from it.
1519                agent.switch_model(resolved.clone());
1520                Ok(FrontendOperationResult::Model {
1521                    model: resolved,
1522                    previous,
1523                })
1524            }
1525        }
1526    }
1527
1528    /// Cancel the current turn through the shared runtime handle.
1529    pub async fn interrupt(&self) -> bool {
1530        let cancel = self
1531            .current_cancel
1532            .lock()
1533            .unwrap_or_else(std::sync::PoisonError::into_inner)
1534            .clone();
1535        match cancel {
1536            Some(cancel) => {
1537                cancel.notify_one();
1538                true
1539            }
1540            None => false,
1541        }
1542    }
1543
1544    /// Return a lock-free runtime snapshot, including while a turn is active.
1545    pub fn status(&self) -> RuntimeStatus {
1546        RuntimeStatus {
1547            session_id: self.session_id.clone(),
1548            model: self.model.clone(),
1549            busy: self.busy.load(Ordering::SeqCst),
1550            shutting_down: self.is_shutting_down(),
1551        }
1552    }
1553
1554    /// Return the tail of the canonical conversation for newly attached
1555    /// frontends. This is SDK state, not a transport-local replay buffer, and
1556    /// remains answerable while a turn owns the agent lock.
1557    pub async fn history(&self, limit: usize) -> Vec<ChatMessage> {
1558        let history = self.history_snapshot.read().await;
1559        let start = history.len().saturating_sub(limit);
1560        history[start..].to_vec()
1561    }
1562
1563    /// Run one caller-owned finalization projection while holding the agent
1564    /// at a quiescent boundary. This is the persistence/inspection seam for
1565    /// local frontends that transfer `Agent` ownership into the SDK runtime;
1566    /// it does not expose a second way to drive the model loop.
1567    pub async fn finalize_with<R>(&self, finalize: impl FnOnce(&SdkAgent) -> R) -> R {
1568        let agent = self.agent.lock().await;
1569        finalize(&agent)
1570    }
1571
1572    /// Request graceful runtime shutdown.  All connected frontends observe
1573    /// the same transition and any in-flight turn is interrupted.
1574    pub async fn shutdown(&self) {
1575        let _barrier = self.shutdown_barrier.lock().await;
1576        // Seal first so no frontend can claim replacement work while the
1577        // active claim is interrupted. Signal transports only after its
1578        // terminal lifecycle is published, so SSE observers receive that
1579        // boundary before their connections close.
1580        self.accepting_submits.store(false, Ordering::SeqCst);
1581        self.interrupt().await;
1582        loop {
1583            let finished = self.turn_finished.notified();
1584            if !self.busy.load(Ordering::SeqCst) {
1585                break;
1586            }
1587            finished.await;
1588        }
1589        self.signal_shutdown();
1590    }
1591
1592    /// Whether `shutdown` has been requested — callers use this to stop
1593    /// accepting new work/connections.
1594    pub fn is_shutting_down(&self) -> bool {
1595        self.shutting_down.load(Ordering::SeqCst)
1596    }
1597
1598    /// Resolves once `shutdown` has been requested. Cheap to call
1599    /// repeatedly/concurrently — every waiter is woken.
1600    pub async fn wait_for_shutdown(&self) {
1601        // `shutting_down` may already be `true` by the time a caller starts
1602        // waiting (e.g. a connection accepted right after `shutdown` fired)
1603        // — check first so this never blocks forever on a signal that
1604        // already happened.
1605        if self.is_shutting_down() {
1606            return;
1607        }
1608        self.shutdown.notified().await;
1609    }
1610
1611    /// Flip `shutting_down` and wake every [`Self::wait_for_shutdown`]
1612    /// waiter — factored out of [`Self::handle_shutdown`] so [`run_stdio`]
1613    /// can raise the EXACT same signal on its OTHER termination path
1614    /// (`reader` hitting EOF with no explicit `shutdown` RPC) without
1615    /// duplicating the store-then-notify sequence. Deliberately does NOT
1616    /// touch `current_cancel` (unlike [`Self::handle_shutdown`], which also
1617    /// interrupts an in-flight turn) — a plain stdio EOF should let an
1618    /// already-accepted turn run to completion and flush its reply, not cut
1619    /// it off.
1620    fn signal_shutdown(&self) {
1621        self.shutting_down.store(true, Ordering::SeqCst);
1622        self.shutdown.notify_waiters();
1623    }
1624
1625    /// Dispatch one already-parsed [`RpcRequest`] to the right method
1626    /// handler. Every recognized method is fully wired to real `Agent`
1627    /// behavior — there is no method that parses but no-ops.
1628    pub async fn handle_request(self: &Arc<Self>, req: RpcRequest) -> Value {
1629        match req.method.as_str() {
1630            "submit" => self.handle_submit(req).await,
1631            "frontend.send_input" => self.handle_frontend_send_input(req),
1632            "interrupt" => self.handle_interrupt(req).await,
1633            "steer" => self.handle_steer(req),
1634            "respond" => self.handle_respond(req),
1635            "status" => self.handle_status(req).await,
1636            "history" => self.handle_history(req).await,
1637            "frontend.describe" => self.handle_frontend_describe(req),
1638            "frontend.attach" => self.handle_frontend_attach(req),
1639            "frontend.invoke" => self.handle_frontend_invoke(req).await,
1640            "shutdown" => self.handle_shutdown(req).await,
1641            other => rpc_error(req.id, -32601, format!("unknown method `{other}`")),
1642        }
1643    }
1644
1645    fn handle_frontend_send_input(self: &Arc<Self>, req: RpcRequest) -> Value {
1646        let Some(prompt) = req.params.get("prompt").and_then(Value::as_str) else {
1647            return rpc_error(
1648                req.id,
1649                -32602,
1650                "frontend.send_input requires a string `params.prompt`",
1651            );
1652        };
1653        let image_urls = match parse_image_urls(&req.params, "frontend.send_input") {
1654            Ok(image_urls) => image_urls,
1655            Err(message) => return rpc_error(req.id, -32602, message),
1656        };
1657        match self.send_input_with_images(prompt.to_string(), image_urls) {
1658            Ok(()) => rpc_ok(req.id, json!({"accepted": true})),
1659            Err(RuntimeSubmitError::Busy) => {
1660                rpc_error(req.id, -32000, "a turn is already in progress")
1661            }
1662            Err(RuntimeSubmitError::Interrupted) => rpc_error(req.id, -32001, "turn interrupted"),
1663            Err(RuntimeSubmitError::Agent(error)) => rpc_error(req.id, -32002, error),
1664        }
1665    }
1666
1667    /// `submit`: drive one turn (`Agent::send`) with `params.prompt`.
1668    /// Refuses (fail, never queues) a second `submit` while one is already
1669    /// in flight — "no method that no-ops": a caller either gets a real
1670    /// answer or an explicit "busy" error, never a silently-dropped
1671    /// request. Races the turn against this call's own fresh cancellation
1672    /// handle so a concurrent `interrupt` can drop it mid-flight — the
1673    /// SAME cancel-safety the CLI's own `race_ctrl_c` relies on
1674    /// (`Agent::send`/`run_loop` only ever mutate `history`/the sidecar
1675    /// BETWEEN `.await` points, never during one, so dropping the future
1676    /// mid-poll always lands in a well-formed place).
1677    async fn handle_submit(&self, req: RpcRequest) -> Value {
1678        let Some(prompt) = req.params.get("prompt").and_then(|v| v.as_str()) else {
1679            return rpc_error(req.id, -32602, "submit requires a string `params.prompt`");
1680        };
1681        match self.submit(prompt).await {
1682            Ok(reply) => rpc_ok(req.id, json!({"reply": reply})),
1683            Err(RuntimeSubmitError::Busy) => rpc_error(
1684                req.id,
1685                -32000,
1686                "a turn is already in progress; `interrupt` it or wait for its response before submitting another",
1687            ),
1688            Err(RuntimeSubmitError::Interrupted) => {
1689                rpc_error(req.id, -32001, "turn interrupted")
1690            }
1691            Err(RuntimeSubmitError::Agent(error)) => rpc_error(req.id, -32002, error),
1692        }
1693    }
1694
1695    /// `interrupt`: cancel the in-flight turn, if any. A no-op-but-honest
1696    /// `{"interrupted": false}` (never an error) when nothing is running —
1697    /// calling `interrupt` with no turn in flight is a normal, harmless
1698    /// race a client can't always avoid (it may not know yet that the
1699    /// previous `submit` just finished).
1700    async fn handle_interrupt(&self, req: RpcRequest) -> Value {
1701        if self.interrupt().await {
1702            rpc_ok(req.id, json!({"interrupted": true}))
1703        } else {
1704            rpc_ok(
1705                req.id,
1706                json!({"interrupted": false, "reason": "no turn in progress"}),
1707            )
1708        }
1709    }
1710
1711    /// `steer`: enqueue an instruction for the next model-loop boundary.
1712    /// This remains responsive while `submit` owns the active agent lock.
1713    fn handle_steer(&self, req: RpcRequest) -> Value {
1714        let Some(prompt) = req.params.get("prompt").and_then(Value::as_str) else {
1715            return rpc_error(req.id, -32602, "steer requires a string `params.prompt`");
1716        };
1717        match self.steer(prompt) {
1718            Ok(()) => rpc_ok(req.id, json!({"queued": true})),
1719            Err(error) => rpc_error(req.id, -32020, error.to_string()),
1720        }
1721    }
1722
1723    fn handle_respond(&self, req: RpcRequest) -> Value {
1724        let response = match req.params.get("response").cloned() {
1725            Some(value) => match serde_json::from_value::<FrontendResponse>(value) {
1726                Ok(response) => response,
1727                Err(error) => return rpc_error(req.id, -32602, error.to_string()),
1728            },
1729            None => return rpc_error(req.id, -32602, "respond requires `params.response`"),
1730        };
1731        match self.respond(response) {
1732            Ok(()) => rpc_ok(req.id, json!({"accepted": true})),
1733            Err(FrontendRuntimeError::UnsupportedAction(_)) => {
1734                rpc_error(req.id, -32020, "frontend respond is not enabled")
1735            }
1736            Err(FrontendRuntimeError::UnknownRequest(id)) => rpc_error(
1737                req.id,
1738                -32021,
1739                format!("frontend request {id} is not pending"),
1740            ),
1741            Err(error) => rpc_error(req.id, -32022, error.to_string()),
1742        }
1743    }
1744
1745    /// `status`: current busy/idle state + the model label. Deliberately
1746    /// never locks `agent` (see [`Self::model`]'s doc comment) — answerable
1747    /// even while a turn is running.
1748    async fn handle_status(&self, req: RpcRequest) -> Value {
1749        rpc_ok(
1750            req.id,
1751            serde_json::to_value(self.status()).unwrap_or_default(),
1752        )
1753    }
1754
1755    /// `history`: bounded canonical transcript replay for a frontend that
1756    /// attached after earlier events were emitted.
1757    async fn handle_history(&self, req: RpcRequest) -> Value {
1758        let limit = req
1759            .params
1760            .get("limit")
1761            .and_then(Value::as_u64)
1762            .unwrap_or(50)
1763            .clamp(1, SERVER_HISTORY_CAPACITY as u64) as usize;
1764        rpc_ok(req.id, json!({"messages": self.history(limit).await}))
1765    }
1766
1767    fn handle_frontend_describe(&self, req: RpcRequest) -> Value {
1768        rpc_ok(
1769            req.id,
1770            serde_json::to_value(self.frontend_descriptor()).unwrap_or_default(),
1771        )
1772    }
1773
1774    fn handle_frontend_attach(&self, req: RpcRequest) -> Value {
1775        let limit = req
1776            .params
1777            .get("limit")
1778            .and_then(Value::as_u64)
1779            .unwrap_or(50)
1780            .clamp(1, SERVER_HISTORY_CAPACITY as u64) as usize;
1781        match self.frontend_snapshot(limit) {
1782            Ok(snapshot) => rpc_ok(req.id, serde_json::to_value(snapshot).unwrap_or_default()),
1783            Err(error) => rpc_error(req.id, -32010, error.to_string()),
1784        }
1785    }
1786
1787    async fn handle_frontend_invoke(&self, req: RpcRequest) -> Value {
1788        let operation = match req.params.get("operation").cloned() {
1789            Some(value) => match serde_json::from_value::<FrontendOperationInvocation>(value) {
1790                Ok(operation) => operation,
1791                Err(error) => return rpc_error(req.id, -32602, error.to_string()),
1792            },
1793            None => {
1794                return rpc_error(
1795                    req.id,
1796                    -32602,
1797                    "frontend.invoke requires `params.operation`",
1798                )
1799            }
1800        };
1801        match self.invoke(operation).await {
1802            Ok(result) => rpc_ok(req.id, serde_json::to_value(result).unwrap_or_default()),
1803            Err(FrontendRuntimeError::UnsupportedOperation(id)) => rpc_error(
1804                req.id,
1805                -32023,
1806                FrontendRuntimeError::UnsupportedOperation(id).to_string(),
1807            ),
1808            Err(FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)) => {
1809                rpc_error(req.id, -32000, "a turn is already in progress")
1810            }
1811            Err(error) => rpc_error(req.id, -32022, error.to_string()),
1812        }
1813    }
1814
1815    /// `shutdown`: request graceful teardown — interrupts any in-flight
1816    /// turn (never leaves a caller hanging on a `submit` that will now
1817    /// never get a transport to answer on) and wakes every
1818    /// [`Self::wait_for_shutdown`] waiter (both transports' accept/read
1819    /// loops select on it, so neither survives as an orphan).
1820    async fn handle_shutdown(&self, req: RpcRequest) -> Value {
1821        self.shutdown().await;
1822        rpc_ok(req.id, json!({"shutting_down": true}))
1823    }
1824}
1825
1826/// Retain one canonical request and at most one resolution for every runtime
1827/// request id. Request history is copied across ordinary ChatMessage snapshot
1828/// boundaries, so blindly copying the prior replay would duplicate it on
1829/// every turn and could eventually leave a later attachment on a stale
1830/// duplicate request. Stable id order and request-before-resolution ordering
1831/// make the reconstructed semantic transcript deterministic.
1832fn compact_frontend_request_history(replay: &VecDeque<FrontendEvent>) -> Vec<Value> {
1833    let mut by_id: BTreeMap<u64, (Option<Value>, Option<Value>)> = BTreeMap::new();
1834    for event in replay {
1835        let (request_id, resolved) = match event.kind.as_str() {
1836            "request" => (
1837                event.payload.pointer("/request/id").and_then(Value::as_u64),
1838                false,
1839            ),
1840            "request_resolved" => (
1841                event.payload.get("request_id").and_then(Value::as_u64),
1842                true,
1843            ),
1844            _ => continue,
1845        };
1846        let Some(request_id) = request_id else {
1847            continue;
1848        };
1849        let entry = by_id.entry(request_id).or_default();
1850        let slot = if resolved { &mut entry.1 } else { &mut entry.0 };
1851        slot.get_or_insert_with(|| event.payload.clone());
1852    }
1853    by_id
1854        .into_values()
1855        .flat_map(|(request, resolution)| request.into_iter().chain(resolution))
1856        .collect()
1857}
1858
1859fn valid_frontend_command_name(name: &str) -> bool {
1860    !name.is_empty()
1861        && !name.starts_with('/')
1862        && name
1863            .chars()
1864            .all(|character| !character.is_whitespace() && !character.is_control())
1865}
1866
1867fn bounded_history_snapshot(history: &[ChatMessage]) -> Vec<ChatMessage> {
1868    let start = history.len().saturating_sub(SERVER_HISTORY_CAPACITY);
1869    history[start..].to_vec()
1870}
1871
1872/// Drive the JSONL-RPC protocol over `reader`/`writer` (the stdio rung —
1873/// parent-process-trusted, no auth token; see the module doc). Each
1874/// request line is dispatched on its OWN spawned task so a `submit`
1875/// in-flight never blocks the reader from picking up a subsequent
1876/// `interrupt`/`status` line — every outgoing line (a response OR an event
1877/// notification) is funneled through one mpsc channel into a single writer
1878/// task, so two concurrent handlers can never interleave a line's bytes.
1879/// Returns once `reader` hits EOF or a `shutdown` request lands.
1880///
1881/// Both termination paths raise `RpcEngine::signal_shutdown` (EOF does it
1882/// directly here; the `shutdown` RPC does it inside
1883/// `RpcEngine::handle_shutdown`), and `event_task` below SELECTS against
1884/// [`RpcEngine::wait_for_shutdown`] rather than merely looping on
1885/// `events.recv()`. This is deliberate: `events.recv()` alone only ends via
1886/// `RecvError::Closed`, which fires only once EVERY clone of
1887/// `engine.events` (the broadcast `Sender`) has dropped — and `engine`
1888/// itself, which keeps that `Sender` alive, is owned by THIS function for
1889/// its whole body. Waiting on `events.recv()` to close would therefore mean
1890/// waiting on `engine` to drop, which can't happen until `event_task`
1891/// itself finishes — a circular wait that never resolves (the bug this fn
1892/// exists to fix). Selecting on the shutdown signal instead lets
1893/// `event_task` end WITHOUT needing `engine`'s refcount to reach zero, so
1894/// there is no orphaned task and no leaked `engine`/`writer_task` blocking
1895/// on it in turn.
1896#[cfg(feature = "adapter-api")]
1897pub async fn run_stdio<R, W>(engine: Arc<RpcEngine>, reader: R, writer: W) -> std::io::Result<()>
1898where
1899    R: AsyncBufRead + Unpin + Send + 'static,
1900    W: AsyncWrite + Unpin + Send + 'static,
1901{
1902    let (out_tx, mut out_rx) = mpsc::unbounded_channel::<Value>();
1903
1904    let writer_task = tokio::spawn(async move {
1905        let mut writer = writer;
1906        while let Some(v) = out_rx.recv().await {
1907            let line = format!("{v}\n");
1908            if writer.write_all(line.as_bytes()).await.is_err() {
1909                break;
1910            }
1911            if writer.flush().await.is_err() {
1912                break;
1913            }
1914        }
1915    });
1916
1917    let mut events = engine.subscribe();
1918    let evt_tx = out_tx.clone();
1919    let evt_engine = engine.clone();
1920    let event_task = tokio::spawn(async move {
1921        loop {
1922            tokio::select! {
1923                biased;
1924                _ = evt_engine.wait_for_shutdown() => break,
1925                recv = events.recv() => {
1926                    match recv {
1927                        Ok(v) => {
1928                            if evt_tx.send(json!({"event": v})).is_err() {
1929                                break;
1930                            }
1931                        }
1932                        Err(broadcast::error::RecvError::Lagged(_)) => continue,
1933                        Err(broadcast::error::RecvError::Closed) => break,
1934                    }
1935                }
1936            }
1937        }
1938    });
1939
1940    let mut reader = reader;
1941    loop {
1942        if engine.is_shutting_down() {
1943            break;
1944        }
1945        tokio::select! {
1946            biased;
1947            _ = engine.wait_for_shutdown() => break,
1948            line = read_bounded_line(&mut reader, SERVER_MAX_LINE_BYTES) => {
1949                match line {
1950                    Ok(None) => {
1951                        // EOF: no explicit `shutdown` RPC landed, but stdin
1952                        // closing is this fn's OTHER documented
1953                        // termination signal — raise the same shutdown
1954                        // signal `event_task` (and any other
1955                        // `wait_for_shutdown` caller) already knows how to
1956                        // watch for, so teardown below actually completes
1957                        // instead of blocking forever on `event_task`.
1958                        engine.signal_shutdown();
1959                        break;
1960                    }
1961                    Ok(Some(text)) => {
1962                        let text = text.trim();
1963                        if text.is_empty() {
1964                            continue;
1965                        }
1966                        match serde_json::from_str::<RpcRequest>(text) {
1967                            Ok(req) => {
1968                                let engine = engine.clone();
1969                                let out_tx = out_tx.clone();
1970                                tokio::spawn(async move {
1971                                    let resp = engine.handle_request(req).await;
1972                                    let _ = out_tx.send(resp);
1973                                });
1974                            }
1975                            Err(e) => {
1976                                let _ = out_tx.send(rpc_error(Value::Null, -32700, format!("parse error: {e}")));
1977                            }
1978                        }
1979                    }
1980                    Err(e) => {
1981                        let _ = out_tx.send(rpc_error(Value::Null, -32700, format!("{e}")));
1982                    }
1983                }
1984            }
1985        }
1986    }
1987    // `event_task` now ends promptly (it's shutdown-signalled above, on
1988    // EITHER termination path) rather than waiting on `engine`'s broadcast
1989    // `Sender` to drop — so awaiting it here no longer deadlocks. Dropping
1990    // this fn's own `out_tx` clone (plus `event_task`'s, once it exits)
1991    // lets `writer_task` see `out_rx.recv()` return `None` once every
1992    // OTHER in-flight per-request task (spawned above, each holding its own
1993    // `out_tx` clone) has also sent its reply and dropped its clone — so
1994    // any reply already accepted before shutdown is still flushed to
1995    // `writer` before this fn returns.
1996    drop(out_tx);
1997    let _ = event_task.await;
1998    let _ = writer_task.await;
1999    Ok(())
2000}
2001
2002/// One parsed HTTP/1.1 request (the minimal subset this module's two
2003/// routes need — no keep-alive, no chunked request bodies).
2004#[cfg(feature = "adapter-api")]
2005struct HttpRequest {
2006    method: String,
2007    /// Path WITHOUT the query string (see `query` for that).
2008    path: String,
2009    query: String,
2010    headers: HashMap<String, String>,
2011    body: Vec<u8>,
2012}
2013
2014/// Read and parse one HTTP/1.1 request from `reader`. `Ok(None)` at a
2015/// clean EOF before any bytes arrive (an idle keep-alive-less connection
2016/// closing). Bounded throughout: the request line and each header line go
2017/// through [`read_bounded_line`] (8KiB — generous for a request
2018/// line/header, far below [`SERVER_MAX_LINE_BYTES`]), the header COUNT is
2019/// capped at [`MAX_HEADER_LINES`], and the body is capped at
2020/// [`SERVER_MAX_LINE_BYTES`].
2021#[cfg(feature = "adapter-api")]
2022async fn read_http_request<R>(reader: &mut R) -> std::io::Result<Option<HttpRequest>>
2023where
2024    R: AsyncBufRead + AsyncRead + Unpin,
2025{
2026    const HEAD_LINE_CAP: usize = 8 * 1024;
2027    let Some(request_line) = read_bounded_line(reader, HEAD_LINE_CAP).await? else {
2028        return Ok(None);
2029    };
2030    let mut parts = request_line.split_whitespace();
2031    let method = parts.next().unwrap_or("").to_string();
2032    let target = parts.next().unwrap_or("").to_string();
2033    if method.is_empty() || target.is_empty() {
2034        return Err(std::io::Error::new(
2035            std::io::ErrorKind::InvalidData,
2036            "malformed request line",
2037        ));
2038    }
2039    let (path, query) = match target.split_once('?') {
2040        Some((p, q)) => (p.to_string(), q.to_string()),
2041        None => (target, String::new()),
2042    };
2043
2044    let mut headers = HashMap::new();
2045    let mut content_length: usize = 0;
2046    for _ in 0..MAX_HEADER_LINES {
2047        let Some(line) = read_bounded_line(reader, HEAD_LINE_CAP).await? else {
2048            return Ok(None);
2049        };
2050        if line.is_empty() {
2051            break;
2052        }
2053        if let Some((k, v)) = line.split_once(':') {
2054            let k = k.trim().to_ascii_lowercase();
2055            let v = v.trim().to_string();
2056            if k == "content-length" {
2057                content_length = v.parse().unwrap_or(0);
2058            }
2059            headers.insert(k, v);
2060        }
2061    }
2062    if content_length > SERVER_MAX_LINE_BYTES {
2063        return Err(std::io::Error::new(
2064            std::io::ErrorKind::InvalidData,
2065            format!("request body exceeded {SERVER_MAX_LINE_BYTES} byte cap"),
2066        ));
2067    }
2068    let mut body = vec![0u8; content_length];
2069    if content_length > 0 {
2070        reader.read_exact(&mut body).await?;
2071    }
2072    Ok(Some(HttpRequest {
2073        method,
2074        path,
2075        query,
2076        headers,
2077        body,
2078    }))
2079}
2080
2081#[cfg(feature = "adapter-api")]
2082async fn write_http_response<W: AsyncWrite + Unpin>(
2083    writer: &mut W,
2084    status: u16,
2085    reason: &str,
2086    content_type: &str,
2087    body: &[u8],
2088) -> std::io::Result<()> {
2089    let head = format!(
2090        "HTTP/1.1 {status} {reason}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
2091        body.len()
2092    );
2093    writer.write_all(head.as_bytes()).await?;
2094    writer.write_all(body).await?;
2095    writer.flush().await
2096}
2097
2098#[cfg(feature = "adapter-api")]
2099fn browser_observer_asset(path: &str) -> Option<(&'static str, &'static [u8])> {
2100    match path {
2101        "/observer" | "/observer/" => Some((
2102            "text/html; charset=utf-8",
2103            include_bytes!("../embedded/frontend-browser/index.html"),
2104        )),
2105        "/observer/app.mjs" => Some((
2106            "text/javascript; charset=utf-8",
2107            include_bytes!("../embedded/frontend-browser/app.mjs"),
2108        )),
2109        "/observer/client.mjs" => Some((
2110            "text/javascript; charset=utf-8",
2111            include_bytes!("../embedded/frontend-browser/client.mjs"),
2112        )),
2113        "/observer/view.mjs" => Some((
2114            "text/javascript; charset=utf-8",
2115            include_bytes!("../embedded/frontend-browser/view.mjs"),
2116        )),
2117        "/observer/style.css" => Some((
2118            "text/css; charset=utf-8",
2119            include_bytes!("../embedded/frontend-browser/style.css"),
2120        )),
2121        "/observer/favicon.svg" | "/favicon.ico" => Some((
2122            "image/svg+xml",
2123            include_bytes!("../embedded/frontend-browser/favicon.svg"),
2124        )),
2125        "/frontend/client.mjs" => Some((
2126            "text/javascript; charset=utf-8",
2127            include_bytes!("../embedded/frontend/client.mjs"),
2128        )),
2129        "/frontend/generated-client.mjs" => Some((
2130            "text/javascript; charset=utf-8",
2131            include_bytes!("../embedded/frontend/generated-client.mjs"),
2132        )),
2133        "/frontend/generated.mjs" => Some((
2134            "text/javascript; charset=utf-8",
2135            include_bytes!("../embedded/frontend/generated.mjs"),
2136        )),
2137        _ => None,
2138    }
2139}
2140
2141#[cfg(feature = "adapter-api")]
2142async fn write_browser_observer_asset<W: AsyncWrite + Unpin>(
2143    writer: &mut W,
2144    content_type: &str,
2145    body: &[u8],
2146) -> std::io::Result<()> {
2147    let head = format!(
2148        "HTTP/1.1 200 OK\r\n\
2149         Content-Type: {content_type}\r\n\
2150         Content-Length: {}\r\n\
2151         Cache-Control: no-store\r\n\
2152         Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'\r\n\
2153         Referrer-Policy: no-referrer\r\n\
2154         X-Content-Type-Options: nosniff\r\n\
2155         Connection: close\r\n\r\n",
2156        body.len()
2157    );
2158    writer.write_all(head.as_bytes()).await?;
2159    writer.write_all(body).await?;
2160    writer.flush().await
2161}
2162
2163/// One bearer credential and its exact SDK authorization grant.
2164#[cfg(feature = "adapter-api")]
2165#[derive(Clone)]
2166pub struct RuntimeHttpCredential {
2167    token: Arc<str>,
2168    authorization: RuntimeAuthorization,
2169    client_id: Option<crate::RuntimeClientId>,
2170    bootstrap: bool,
2171    runtime_id: Option<Arc<str>>,
2172    generation: Option<[u8; 16]>,
2173    revocation: Option<Arc<RuntimeCredentialRevocation>>,
2174}
2175
2176#[cfg(feature = "adapter-api")]
2177impl RuntimeHttpCredential {
2178    /// Create a scoped credential. Tokens are deliberately private and never
2179    /// implement `Debug` or serialization.
2180    pub fn new(token: impl Into<Arc<str>>, authorization: RuntimeAuthorization) -> Self {
2181        Self {
2182            token: token.into(),
2183            authorization,
2184            client_id: None,
2185            bootstrap: false,
2186            runtime_id: None,
2187            generation: None,
2188            revocation: None,
2189        }
2190    }
2191
2192    /// Full owner credential preserving the historical `run_http` contract.
2193    ///
2194    /// New frontend-host paths must keep this bootstrap credential private and
2195    /// exchange it through the local mint endpoint for a bound frontend grant.
2196    pub fn owner(token: impl Into<Arc<str>>) -> Self {
2197        Self {
2198            token: token.into(),
2199            authorization: RuntimeAuthorization::owner(),
2200            client_id: None,
2201            bootstrap: true,
2202            runtime_id: None,
2203            generation: None,
2204            revocation: None,
2205        }
2206    }
2207
2208    /// Read-only observer credential.
2209    pub fn observer(token: impl Into<Arc<str>>) -> Self {
2210        Self::new(token, RuntimeAuthorization::observer())
2211    }
2212
2213    fn frontend(
2214        token: impl Into<Arc<str>>,
2215        client_id: crate::RuntimeClientId,
2216        authorization: RuntimeAuthorization,
2217        runtime_id: impl Into<Arc<str>>,
2218        generation: [u8; 16],
2219    ) -> Self {
2220        Self {
2221            token: token.into(),
2222            authorization,
2223            client_id: Some(client_id),
2224            bootstrap: false,
2225            runtime_id: Some(runtime_id.into()),
2226            generation: Some(generation),
2227            revocation: Some(Arc::new(RuntimeCredentialRevocation::new())),
2228        }
2229    }
2230}
2231
2232#[cfg(feature = "adapter-api")]
2233struct AuthenticatedRuntimeHttpCredential {
2234    authorization: RuntimeAuthorization,
2235    client_id: Option<crate::RuntimeClientId>,
2236    bootstrap: bool,
2237    revocation: Option<tokio::sync::watch::Receiver<bool>>,
2238    attachment: Option<RuntimeCredentialAttachment>,
2239    via_bearer_header: bool,
2240}
2241
2242#[cfg(feature = "adapter-api")]
2243struct RuntimeCredentialRevocation {
2244    signal: tokio::sync::watch::Sender<bool>,
2245    active_attachments: AtomicUsize,
2246    drained: tokio::sync::Notify,
2247}
2248
2249#[cfg(feature = "adapter-api")]
2250impl RuntimeCredentialRevocation {
2251    fn new() -> Self {
2252        let (signal, _) = tokio::sync::watch::channel(false);
2253        Self {
2254            signal,
2255            active_attachments: AtomicUsize::new(0),
2256            drained: tokio::sync::Notify::new(),
2257        }
2258    }
2259
2260    fn register(self: &Arc<Self>) -> RuntimeCredentialAttachment {
2261        self.active_attachments.fetch_add(1, Ordering::AcqRel);
2262        RuntimeCredentialAttachment {
2263            revocation: self.clone(),
2264        }
2265    }
2266
2267    async fn revoke_and_wait(&self) {
2268        let _ = self.signal.send(true);
2269        loop {
2270            let drained = self.drained.notified();
2271            if self.active_attachments.load(Ordering::Acquire) == 0 {
2272                return;
2273            }
2274            drained.await;
2275        }
2276    }
2277}
2278
2279#[cfg(feature = "adapter-api")]
2280struct RuntimeCredentialAttachment {
2281    revocation: Arc<RuntimeCredentialRevocation>,
2282}
2283
2284#[cfg(feature = "adapter-api")]
2285impl Drop for RuntimeCredentialAttachment {
2286    fn drop(&mut self) {
2287        if self
2288            .revocation
2289            .active_attachments
2290            .fetch_sub(1, Ordering::AcqRel)
2291            == 1
2292        {
2293            // Registry removal makes the first revoke the sole waiter for a
2294            // credential; concurrent revokes find no credential. `notify_one`
2295            // stores a permit if this lands between the counter check and the
2296            // first poll of `notified()`, preventing a lost wakeup.
2297            self.revocation.drained.notify_one();
2298        }
2299    }
2300}
2301
2302#[cfg(feature = "adapter-api")]
2303struct IssuedRuntimeHttpCredential(Arc<str>);
2304
2305#[cfg(feature = "adapter-api")]
2306impl IssuedRuntimeHttpCredential {
2307    fn as_bytes(&self) -> &[u8] {
2308        self.0.as_bytes()
2309    }
2310}
2311
2312#[cfg(feature = "adapter-api")]
2313struct RuntimeHttpCredentialRegistry {
2314    credentials: StdMutex<Vec<RuntimeHttpCredential>>,
2315    runtime_id: String,
2316    generation: [u8; 16],
2317}
2318
2319#[cfg(feature = "adapter-api")]
2320impl RuntimeHttpCredentialRegistry {
2321    fn new(
2322        runtime_id: impl Into<String>,
2323        credentials: Vec<RuntimeHttpCredential>,
2324    ) -> std::io::Result<Arc<Self>> {
2325        let mut generation = [0_u8; 16];
2326        getrandom::getrandom(&mut generation).map_err(|error| {
2327            std::io::Error::other(format!(
2328                "cannot create runtime credential generation: {error}"
2329            ))
2330        })?;
2331        Ok(Arc::new(Self {
2332            credentials: StdMutex::new(credentials),
2333            runtime_id: runtime_id.into(),
2334            generation,
2335        }))
2336    }
2337
2338    fn authenticate(&self, request: &HttpRequest) -> Option<AuthenticatedRuntimeHttpCredential> {
2339        let credentials = self
2340            .credentials
2341            .lock()
2342            .unwrap_or_else(std::sync::PoisonError::into_inner);
2343        debug_assert!(credentials.iter().all(|credential| {
2344            credential.client_id.is_none()
2345                || (credential.runtime_id.as_deref() == Some(self.runtime_id.as_str())
2346                    && credential.generation == Some(self.generation))
2347        }));
2348        check_auth(request, &credentials)
2349    }
2350
2351    fn issue_frontend(
2352        &self,
2353        client_id: crate::RuntimeClientId,
2354        observer: bool,
2355    ) -> std::io::Result<IssuedRuntimeHttpCredential> {
2356        let authorization = if observer {
2357            RuntimeAuthorization::observer()
2358        } else {
2359            RuntimeAuthorization::interactive()
2360        };
2361        for _ in 0..3 {
2362            let mut secret = [0_u8; 32];
2363            getrandom::getrandom(&mut secret).map_err(|error| {
2364                std::io::Error::other(format!("cannot mint frontend credential: {error}"))
2365            })?;
2366            let token: Arc<str> = encode_credential(&secret).into();
2367            secret.fill(0);
2368            let mut credentials = self
2369                .credentials
2370                .lock()
2371                .unwrap_or_else(std::sync::PoisonError::into_inner);
2372            if credentials
2373                .iter()
2374                .any(|credential| constant_time_eq(token.as_bytes(), credential.token.as_bytes()))
2375            {
2376                continue;
2377            }
2378            credentials.push(RuntimeHttpCredential::frontend(
2379                token.clone(),
2380                client_id,
2381                authorization,
2382                self.runtime_id.clone(),
2383                self.generation,
2384            ));
2385            return Ok(IssuedRuntimeHttpCredential(token));
2386        }
2387        Err(std::io::Error::new(
2388            std::io::ErrorKind::AlreadyExists,
2389            "frontend credential collision limit exceeded",
2390        ))
2391    }
2392
2393    async fn revoke_client(&self, client_id: &crate::RuntimeClientId) -> bool {
2394        // Authentication removal and attachment registration share this lock,
2395        // so no credential-owned channel can appear after the removal point.
2396        let revocations = {
2397            let mut credentials = self
2398                .credentials
2399                .lock()
2400                .unwrap_or_else(std::sync::PoisonError::into_inner);
2401            let mut revocations = Vec::new();
2402            credentials.retain(|credential| {
2403                if credential.client_id.as_ref() == Some(client_id) {
2404                    if let Some(revocation) = &credential.revocation {
2405                        revocations.push(revocation.clone());
2406                    }
2407                    false
2408                } else {
2409                    true
2410                }
2411            });
2412            revocations
2413        };
2414        let revoked = !revocations.is_empty();
2415        for revocation in revocations {
2416            revocation.revoke_and_wait().await;
2417        }
2418        revoked
2419    }
2420}
2421
2422#[cfg(feature = "adapter-api")]
2423fn encode_credential(secret: &[u8; 32]) -> String {
2424    const HEX: &[u8; 16] = b"0123456789abcdef";
2425    let mut encoded = String::with_capacity(64);
2426    for byte in secret {
2427        encoded.push(HEX[(byte >> 4) as usize] as char);
2428        encoded.push(HEX[(byte & 0x0f) as usize] as char);
2429    }
2430    encoded
2431}
2432
2433/// Does `req` carry a recognized bearer credential? Checked two ways: the
2434/// standard `Authorization: Bearer <token>` header, or, for unbound legacy
2435/// credentials only, a `?token=` query-string parameter (kept for
2436/// `GET /events`, since browser `EventSource` cannot set custom headers).
2437/// Scoped frontend credentials are header-only. Compared with
2438/// [`constant_time_eq`].
2439#[cfg(feature = "adapter-api")]
2440fn check_auth(
2441    req: &HttpRequest,
2442    credentials: &[RuntimeHttpCredential],
2443) -> Option<AuthenticatedRuntimeHttpCredential> {
2444    if let Some(auth) = req.headers.get("authorization") {
2445        if let Some(t) = auth.strip_prefix("Bearer ") {
2446            for credential in credentials {
2447                if constant_time_eq(t.as_bytes(), credential.token.as_bytes()) {
2448                    return Some(AuthenticatedRuntimeHttpCredential {
2449                        authorization: credential.authorization.clone(),
2450                        client_id: credential.client_id.clone(),
2451                        bootstrap: credential.bootstrap,
2452                        revocation: credential
2453                            .revocation
2454                            .as_ref()
2455                            .map(|revocation| revocation.signal.subscribe()),
2456                        attachment: credential.revocation.as_ref().and_then(|revocation| {
2457                            matches!(req.path.as_str(), "/events" | "/frontend/events")
2458                                .then(|| revocation.register())
2459                        }),
2460                        via_bearer_header: true,
2461                    });
2462                }
2463            }
2464        }
2465    }
2466    for pair in req.query.split('&') {
2467        if let Some((k, v)) = pair.split_once('=') {
2468            if k == "token" {
2469                for credential in credentials
2470                    .iter()
2471                    .filter(|credential| credential.client_id.is_none())
2472                {
2473                    if constant_time_eq(v.as_bytes(), credential.token.as_bytes()) {
2474                        return Some(AuthenticatedRuntimeHttpCredential {
2475                            authorization: credential.authorization.clone(),
2476                            client_id: credential.client_id.clone(),
2477                            bootstrap: credential.bootstrap,
2478                            revocation: None,
2479                            attachment: None,
2480                            via_bearer_header: false,
2481                        });
2482                    }
2483                }
2484            }
2485        }
2486    }
2487    None
2488}
2489
2490#[cfg(feature = "adapter-api")]
2491fn coordinated_http_client(
2492    request: &HttpRequest,
2493    coordinator: &Arc<CoordinatedRuntime>,
2494    credential: AuthenticatedRuntimeHttpCredential,
2495) -> Result<Arc<CoordinatedRuntimeClient>, crate::RuntimeLeaseError> {
2496    // Old authenticated API clients predate explicit client ids. Preserve
2497    // them as one named compatibility controller; current SDK clients always
2498    // send a random stable id and therefore coordinate independently.
2499    let supplied_client_id = request
2500        .headers
2501        .get("x-supercode-client-id")
2502        .map(String::as_str);
2503    let client_id = match credential.client_id.as_ref() {
2504        Some(bound) if supplied_client_id == Some(bound.as_str()) => bound.as_str(),
2505        Some(_) => return Err(crate::RuntimeLeaseError::InvalidClientId),
2506        None => supplied_client_id.unwrap_or("legacy-owner"),
2507    };
2508    let mut authorization = credential.authorization;
2509    if let Some(requested) = request.headers.get("x-supercode-permissions") {
2510        authorization = authorization.restrict_to(&RuntimeAuthorization::parse_header(requested)?);
2511    }
2512    Ok(coordinator.client(RuntimeClientId::parse(client_id)?, authorization))
2513}
2514
2515#[cfg(feature = "adapter-api")]
2516async fn coordinated_runtime_rpc(
2517    client: Arc<CoordinatedRuntimeClient>,
2518    request: RpcRequest,
2519) -> Value {
2520    let id = request.id.clone();
2521    let method = crate::FrontendFacadeMethod::from_wire_name(&request.method);
2522    let result = match method {
2523        Some(crate::FrontendFacadeMethod::AcquireControl) => client
2524            .acquire_control()
2525            .and_then(|snapshot| serde_json::to_value(snapshot).map_err(json_sdk_error)),
2526        Some(crate::FrontendFacadeMethod::TakeControl) => client
2527            .take_control()
2528            .and_then(|snapshot| serde_json::to_value(snapshot).map_err(json_sdk_error)),
2529        Some(crate::FrontendFacadeMethod::Heartbeat) => client
2530            .heartbeat()
2531            .and_then(|snapshot| serde_json::to_value(snapshot).map_err(json_sdk_error)),
2532        Some(crate::FrontendFacadeMethod::Lease) => client
2533            .lease_snapshot()
2534            .and_then(|snapshot| serde_json::to_value(snapshot).map_err(json_sdk_error)),
2535        Some(crate::FrontendFacadeMethod::Detach) => {
2536            serde_json::to_value(client.detach()).map_err(json_sdk_error)
2537        }
2538        Some(crate::FrontendFacadeMethod::Close) => match client.close().await {
2539            Ok(()) => Ok(json!({"closed":true})),
2540            Err(error) => Err(error),
2541        },
2542        None if request.method == "shutdown" => match client.close().await {
2543            Ok(()) => Ok(json!({"shutting_down":true})),
2544            Err(error) => Err(error),
2545        },
2546        _ => return frontend_http_rpc(client, request).await,
2547    };
2548    match result {
2549        Ok(value) => rpc_ok(id, value),
2550        Err(error) => sdk_runtime_rpc_error(id, -32002, &error),
2551    }
2552}
2553
2554#[cfg(feature = "adapter-api")]
2555fn json_sdk_error(error: serde_json::Error) -> FrontendRuntimeError {
2556    FrontendRuntimeError::Transport(error.to_string())
2557}
2558
2559/// The runtime's own loopback credential door: `POST` mint and revoke.
2560///
2561/// It lives in ONE place and both HTTP conns call it, because the live-runtime
2562/// receipt names a single `base_url` and `sdk/typescript/live-runtime.mjs`
2563/// mints against exactly that URL — "the runtime's own loopback mint door" —
2564/// while warning that two copies of a security-relevant door drift. A hosted
2565/// runtime registers its receipt with the FRONTEND conn's address
2566/// (`harness_service.rs`'s `insert_hosted_runtime`), so that conn has to serve
2567/// this door or the receipt points at a 404.
2568///
2569/// Returns `None` when the request is not one of these two paths.
2570#[cfg(feature = "adapter-api")]
2571async fn serve_frontend_credential_door<W: AsyncWrite + Unpin>(
2572    req: &HttpRequest,
2573    write_half: &mut W,
2574    peer_is_loopback: bool,
2575    credential: &AuthenticatedRuntimeHttpCredential,
2576    credentials: &RuntimeHttpCredentialRegistry,
2577    coordinator: &Arc<CoordinatedRuntime>,
2578) -> Option<std::io::Result<()>> {
2579    if matches!(
2580        req.path.as_str(),
2581        "/_supercode/frontend-credentials/mint" | "/_supercode/frontend-credentials/revoke"
2582    ) && !credential.via_bearer_header
2583    {
2584        let body =
2585            sdk_runtime_rpc_error(Value::Null, -32030, &FrontendRuntimeError::Unauthenticated)
2586                .to_string();
2587        return Some(
2588            write_http_response(
2589                write_half,
2590                401,
2591                "Unauthorized",
2592                "application/json",
2593                body.as_bytes(),
2594            )
2595            .await,
2596        );
2597    }
2598    if req.path == "/_supercode/frontend-credentials/mint" {
2599        if req.method != "POST" || !peer_is_loopback || !credential.bootstrap {
2600            let body = sdk_runtime_rpc_error(
2601                Value::Null,
2602                -32031,
2603                &FrontendRuntimeError::Unauthorized {
2604                    permission: "bootstrap".into(),
2605                },
2606            )
2607            .to_string();
2608            return Some(
2609                write_http_response(
2610                    write_half,
2611                    403,
2612                    "Forbidden",
2613                    "application/json",
2614                    body.as_bytes(),
2615                )
2616                .await,
2617            );
2618        }
2619        let request: Value = match serde_json::from_slice(&req.body) {
2620            Ok(request) => request,
2621            Err(error) => {
2622                return Some(
2623                    write_http_response(
2624                        write_half,
2625                        400,
2626                        "Bad Request",
2627                        "application/json",
2628                        format!("{{\"error\":{}}}", json!(error.to_string())).as_bytes(),
2629                    )
2630                    .await,
2631                );
2632            }
2633        };
2634        let Some(client_id) = request.get("clientId").and_then(Value::as_str) else {
2635            return Some(
2636                write_http_response(
2637                    write_half,
2638                    400,
2639                    "Bad Request",
2640                    "application/json",
2641                    b"{\"error\":\"mint request omitted clientId\"}",
2642                )
2643                .await,
2644            );
2645        };
2646        let client_id = match crate::RuntimeClientId::parse(client_id) {
2647            Ok(client_id) => client_id,
2648            Err(error) => {
2649                return Some(
2650                    write_http_response(
2651                        write_half,
2652                        400,
2653                        "Bad Request",
2654                        "application/json",
2655                        format!("{{\"error\":{}}}", json!(error.to_string())).as_bytes(),
2656                    )
2657                    .await,
2658                );
2659            }
2660        };
2661        let observer = match request.get("grant").and_then(Value::as_str) {
2662            Some("observer") => true,
2663            Some("interactive") => false,
2664            _ => {
2665                return Some(
2666                    write_http_response(
2667                        write_half,
2668                        400,
2669                        "Bad Request",
2670                        "application/json",
2671                        b"{\"error\":\"grant must be observer or interactive\"}",
2672                    )
2673                    .await,
2674                );
2675            }
2676        };
2677        let token = match credentials.issue_frontend(client_id, observer) {
2678            Ok(token) => token,
2679            Err(error) => return Some(Err(error)),
2680        };
2681        return Some(
2682            write_http_response(
2683                write_half,
2684                200,
2685                "OK",
2686                "application/octet-stream",
2687                token.as_bytes(),
2688            )
2689            .await,
2690        );
2691    }
2692
2693    if req.path == "/_supercode/frontend-credentials/revoke" {
2694        if req.method != "POST" || !peer_is_loopback || !credential.bootstrap {
2695            let body = sdk_runtime_rpc_error(
2696                Value::Null,
2697                -32031,
2698                &FrontendRuntimeError::Unauthorized {
2699                    permission: "bootstrap".into(),
2700                },
2701            )
2702            .to_string();
2703            return Some(
2704                write_http_response(
2705                    write_half,
2706                    403,
2707                    "Forbidden",
2708                    "application/json",
2709                    body.as_bytes(),
2710                )
2711                .await,
2712            );
2713        }
2714        let request: Value = match serde_json::from_slice(&req.body) {
2715            Ok(request) => request,
2716            Err(error) => {
2717                return Some(
2718                    write_http_response(
2719                        write_half,
2720                        400,
2721                        "Bad Request",
2722                        "application/json",
2723                        format!("{{\"error\":{}}}", json!(error.to_string())).as_bytes(),
2724                    )
2725                    .await,
2726                );
2727            }
2728        };
2729        let Some(client_id) = request.get("clientId").and_then(Value::as_str) else {
2730            return Some(
2731                write_http_response(
2732                    write_half,
2733                    400,
2734                    "Bad Request",
2735                    "application/json",
2736                    b"{\"error\":\"revoke request omitted clientId\"}",
2737                )
2738                .await,
2739            );
2740        };
2741        let client_id = match crate::RuntimeClientId::parse(client_id) {
2742            Ok(client_id) => client_id,
2743            Err(error) => {
2744                return Some(
2745                    write_http_response(
2746                        write_half,
2747                        400,
2748                        "Bad Request",
2749                        "application/json",
2750                        format!("{{\"error\":{}}}", json!(error.to_string())).as_bytes(),
2751                    )
2752                    .await,
2753                );
2754            }
2755        };
2756        let revoked = credentials.revoke_client(&client_id).await;
2757        if revoked {
2758            coordinator
2759                .client(client_id, RuntimeAuthorization::observer())
2760                .detach();
2761        }
2762        return Some(
2763            write_http_response(
2764                write_half,
2765                200,
2766                "OK",
2767                "application/json",
2768                if revoked {
2769                    b"{\"revoked\":true}"
2770                } else {
2771                    b"{\"revoked\":false}"
2772                },
2773            )
2774            .await,
2775        );
2776    }
2777
2778    None
2779}
2780
2781#[cfg(feature = "adapter-api")]
2782async fn handle_http_conn(
2783    stream: tokio::net::TcpStream,
2784    engine: Arc<RpcEngine>,
2785    coordinator: Arc<CoordinatedRuntime>,
2786    credentials: Arc<RuntimeHttpCredentialRegistry>,
2787) -> std::io::Result<()> {
2788    let peer_is_loopback = stream.peer_addr()?.ip().is_loopback();
2789    let (read_half, mut write_half) = stream.into_split();
2790    let mut reader = tokio::io::BufReader::new(read_half);
2791    let Some(req) = read_http_request(&mut reader).await? else {
2792        return Ok(());
2793    };
2794
2795    // Static observer assets contain no runtime descriptor or session content.
2796    // Serve them before authentication so a user can load the credential form;
2797    // every SDK request made by that page remains bearer-authenticated below.
2798    if req.method == "GET" {
2799        if let Some((content_type, body)) = browser_observer_asset(&req.path) {
2800            return write_browser_observer_asset(&mut write_half, content_type, body).await;
2801        }
2802    }
2803
2804    let Some(credential) = credentials.authenticate(&req) else {
2805        let body =
2806            sdk_runtime_rpc_error(Value::Null, -32030, &FrontendRuntimeError::Unauthenticated)
2807                .to_string();
2808        return write_http_response(
2809            &mut write_half,
2810            401,
2811            "Unauthorized",
2812            "application/json",
2813            body.as_bytes(),
2814        )
2815        .await;
2816    };
2817
2818    if let Some(result) = serve_frontend_credential_door(
2819        &req,
2820        &mut write_half,
2821        peer_is_loopback,
2822        &credential,
2823        &credentials,
2824        &coordinator,
2825    )
2826    .await
2827    {
2828        return result;
2829    }
2830
2831    let mut revocation = credential.revocation.clone();
2832    let mut attachment = credential.attachment;
2833    let credential = AuthenticatedRuntimeHttpCredential {
2834        authorization: credential.authorization,
2835        client_id: credential.client_id,
2836        bootstrap: credential.bootstrap,
2837        revocation: None,
2838        attachment: None,
2839        via_bearer_header: credential.via_bearer_header,
2840    };
2841    let client = match coordinated_http_client(&req, &coordinator, credential) {
2842        Ok(client) => client,
2843        Err(error) => {
2844            let permission = match error {
2845                crate::RuntimeLeaseError::InvalidClientId => "client_id",
2846                crate::RuntimeLeaseError::InvalidAuthorization => "authorization",
2847                _ => "runtime",
2848            };
2849            let body = sdk_runtime_rpc_error(
2850                Value::Null,
2851                -32031,
2852                &FrontendRuntimeError::Unauthorized {
2853                    permission: permission.into(),
2854                },
2855            )
2856            .to_string();
2857            return write_http_response(
2858                &mut write_half,
2859                403,
2860                "Forbidden",
2861                "application/json",
2862                body.as_bytes(),
2863            )
2864            .await;
2865        }
2866    };
2867
2868    match (req.method.as_str(), req.path.as_str()) {
2869        ("POST", "/rpc") => {
2870            let body_text = String::from_utf8_lossy(&req.body);
2871            let resp = match serde_json::from_str::<RpcRequest>(&body_text) {
2872                Ok(rpc_req) if matches!(rpc_req.method.as_str(), "status" | "history") => {
2873                    engine.handle_request(rpc_req).await
2874                }
2875                Ok(rpc_req) => coordinated_runtime_rpc(client.clone(), rpc_req).await,
2876                Err(e) => rpc_error(Value::Null, -32700, format!("parse error: {e}")),
2877            };
2878            let body = resp.to_string();
2879            write_http_response(
2880                &mut write_half,
2881                200,
2882                "OK",
2883                "application/json",
2884                body.as_bytes(),
2885            )
2886            .await
2887        }
2888        ("GET", "/events") => {
2889            if let Err(error) = client.observe() {
2890                let body = sdk_runtime_rpc_error(Value::Null, -32002, &error).to_string();
2891                return write_http_response(
2892                    &mut write_half,
2893                    403,
2894                    "Forbidden",
2895                    "application/json",
2896                    body.as_bytes(),
2897                )
2898                .await;
2899            }
2900            // Arm the subscriber before acknowledging SSE readiness. Otherwise a
2901            // client can receive 200, immediately submit a turn on /rpc, and lose
2902            // every event emitted before this branch reaches `subscribe()`.
2903            let mut events = engine.subscribe();
2904            let head = "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n";
2905            if write_half.write_all(head.as_bytes()).await.is_err() {
2906                client.detach();
2907                return Ok(());
2908            }
2909            let _ = write_half.flush().await;
2910            loop {
2911                tokio::select! {
2912                    biased;
2913                    recv = events.recv() => {
2914                        match recv {
2915                            Ok(v) => {
2916                                let line = format!("data: {v}\n\n");
2917                                if write_half.write_all(line.as_bytes()).await.is_err() {
2918                                    break;
2919                                }
2920                                if write_half.flush().await.is_err() {
2921                                    break;
2922                                }
2923                            }
2924                            Err(broadcast::error::RecvError::Lagged(_)) => continue,
2925                            Err(broadcast::error::RecvError::Closed) => break,
2926                        }
2927                    }
2928                    // `RpcEngine::shutdown` publishes the turn terminal
2929                    // event before signaling shutdown. Drain that already-
2930                    // buffered event first so a close racing an active turn
2931                    // cannot make observers miss its final state.
2932                    _ = engine.wait_for_shutdown() => break,
2933                    _ = wait_for_credential_revocation(&mut revocation) => break,
2934                    _ = reader.read_u8() => break,
2935                }
2936            }
2937            let _ = write_half.shutdown().await;
2938            client.detach();
2939            drop(attachment.take());
2940            Ok(())
2941        }
2942        ("GET", "/frontend/events") => {
2943            if let Err(error) = client.observe() {
2944                let body = sdk_runtime_rpc_error(Value::Null, -32002, &error).to_string();
2945                return write_http_response(
2946                    &mut write_half,
2947                    403,
2948                    "Forbidden",
2949                    "application/json",
2950                    body.as_bytes(),
2951                )
2952                .await;
2953            }
2954            // Subscribe before acknowledging the stream. Once the client has
2955            // received the 200 response, every later frontend event is either
2956            // buffered here or delivered live; there is no header/subscription
2957            // race at connection startup.
2958            let mut events = engine.frontend_subscribe();
2959            let head = "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n";
2960            if write_half.write_all(head.as_bytes()).await.is_err() {
2961                client.detach();
2962                return Ok(());
2963            }
2964            let _ = write_half.flush().await;
2965            loop {
2966                tokio::select! {
2967                    biased;
2968                    recv = events.recv() => {
2969                        match recv {
2970                            Ok(event) => {
2971                                let value = serde_json::to_string(&event).unwrap_or_default();
2972                                let line = format!("data: {value}\n\n");
2973                                if write_half.write_all(line.as_bytes()).await.is_err() {
2974                                    break;
2975                                }
2976                                if write_half.flush().await.is_err() {
2977                                    break;
2978                                }
2979                            }
2980                            Err(broadcast::error::RecvError::Lagged(_)) => break,
2981                            Err(broadcast::error::RecvError::Closed) => break,
2982                        }
2983                    }
2984                    // Shutdown is signaled only after the terminal frontend
2985                    // event is published. Prefer the receiver when both are
2986                    // ready so every observer sees that final sequence.
2987                    _ = engine.wait_for_shutdown() => break,
2988                    _ = wait_for_credential_revocation(&mut revocation) => break,
2989                    _ = reader.read_u8() => break,
2990                }
2991            }
2992            let _ = write_half.shutdown().await;
2993            client.detach();
2994            drop(attachment.take());
2995            Ok(())
2996        }
2997        _ => {
2998            write_http_response(
2999                &mut write_half,
3000                404,
3001                "Not Found",
3002                "application/json",
3003                b"{\"error\":\"not found\"}",
3004            )
3005            .await
3006        }
3007    }
3008}
3009
3010#[cfg(feature = "adapter-api")]
3011async fn wait_for_credential_revocation(receiver: &mut Option<tokio::sync::watch::Receiver<bool>>) {
3012    let Some(receiver) = receiver else {
3013        std::future::pending::<()>().await;
3014        return;
3015    };
3016    if *receiver.borrow() {
3017        return;
3018    }
3019    while receiver.changed().await.is_ok() {
3020        if *receiver.borrow() {
3021            return;
3022        }
3023    }
3024}
3025
3026#[cfg(feature = "adapter-api")]
3027async fn handle_frontend_http_conn(
3028    stream: tokio::net::TcpStream,
3029    coordinator: Arc<CoordinatedRuntime>,
3030    events: broadcast::Sender<FrontendEvent>,
3031    credentials: Arc<RuntimeHttpCredentialRegistry>,
3032) -> std::io::Result<()> {
3033    let peer_is_loopback = stream.peer_addr()?.ip().is_loopback();
3034    let (read_half, mut write_half) = stream.into_split();
3035    let mut reader = tokio::io::BufReader::new(read_half);
3036    let Some(req) = read_http_request(&mut reader).await? else {
3037        return Ok(());
3038    };
3039    if req.method == "GET" {
3040        if let Some((content_type, body)) = browser_observer_asset(&req.path) {
3041            return write_browser_observer_asset(&mut write_half, content_type, body).await;
3042        }
3043    }
3044    let Some(credential) = credentials.authenticate(&req) else {
3045        return write_http_response(
3046            &mut write_half,
3047            401,
3048            "Unauthorized",
3049            "application/json",
3050            b"{\"error\":\"missing or invalid bearer token\"}",
3051        )
3052        .await;
3053    };
3054    // The receipt a hosted runtime registers names THIS listener, so the mint
3055    // door a frontend is told to use has to answer here.
3056    if let Some(result) = serve_frontend_credential_door(
3057        &req,
3058        &mut write_half,
3059        peer_is_loopback,
3060        &credential,
3061        &credentials,
3062        &coordinator,
3063    )
3064    .await
3065    {
3066        return result;
3067    }
3068    let client = match coordinated_http_client(&req, &coordinator, credential) {
3069        Ok(client) => client,
3070        Err(error) => {
3071            let body = json!({"error":error.to_string()}).to_string();
3072            return write_http_response(
3073                &mut write_half,
3074                400,
3075                "Bad Request",
3076                "application/json",
3077                body.as_bytes(),
3078            )
3079            .await;
3080        }
3081    };
3082    match (req.method.as_str(), req.path.as_str()) {
3083        ("POST", "/rpc") => {
3084            let body_text = String::from_utf8_lossy(&req.body);
3085            let response = match serde_json::from_str::<RpcRequest>(&body_text) {
3086                Ok(request) => coordinated_runtime_rpc(client.clone(), request).await,
3087                Err(error) => rpc_error(Value::Null, -32700, format!("parse error: {error}")),
3088            };
3089            let body = response.to_string();
3090            write_http_response(
3091                &mut write_half,
3092                200,
3093                "OK",
3094                "application/json",
3095                body.as_bytes(),
3096            )
3097            .await
3098        }
3099        ("GET", "/frontend/events") => {
3100            if let Err(error) = client.observe() {
3101                let body = sdk_runtime_rpc_error(Value::Null, -32002, &error).to_string();
3102                return write_http_response(
3103                    &mut write_half,
3104                    403,
3105                    "Forbidden",
3106                    "application/json",
3107                    body.as_bytes(),
3108                )
3109                .await;
3110            }
3111            let mut receiver = events.subscribe();
3112            let head = "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n";
3113            if write_half.write_all(head.as_bytes()).await.is_err() {
3114                client.detach();
3115                return Ok(());
3116            }
3117            let _ = write_half.flush().await;
3118            loop {
3119                tokio::select! {
3120                    _ = reader.read_u8() => break,
3121                    event = receiver.recv() => match event {
3122                        Ok(event) => {
3123                            let value = serde_json::to_string(&event).unwrap_or_default();
3124                            let line = format!("data: {value}\n\n");
3125                            if write_half.write_all(line.as_bytes()).await.is_err() || write_half.flush().await.is_err() {
3126                                break;
3127                            }
3128                        }
3129                        Err(broadcast::error::RecvError::Lagged(_)) => break,
3130                        Err(broadcast::error::RecvError::Closed) => break,
3131                    }
3132                }
3133            }
3134            client.detach();
3135            Ok(())
3136        }
3137        _ => {
3138            write_http_response(
3139                &mut write_half,
3140                404,
3141                "Not Found",
3142                "application/json",
3143                b"{\"error\":\"not found\"}",
3144            )
3145            .await
3146        }
3147    }
3148}
3149
3150#[cfg(feature = "adapter-api")]
3151async fn frontend_http_rpc(runtime: Arc<dyn FrontendRuntime>, request: RpcRequest) -> Value {
3152    let id = request.id;
3153    let Some(method) = crate::FrontendFacadeMethod::from_wire_name(&request.method) else {
3154        return rpc_error(id, -32601, format!("unknown method `{}`", request.method));
3155    };
3156    match method {
3157        crate::FrontendFacadeMethod::Describe => match runtime.describe().await {
3158            Ok(descriptor) => rpc_ok(id, serde_json::to_value(descriptor).unwrap_or_default()),
3159            Err(error) => sdk_runtime_rpc_error(id, -32010, &error),
3160        },
3161        crate::FrontendFacadeMethod::Attach => {
3162            let limit = request
3163                .params
3164                .get("limit")
3165                .and_then(Value::as_u64)
3166                .unwrap_or(50)
3167                .clamp(1, SERVER_HISTORY_CAPACITY as u64) as usize;
3168            match runtime.attach(limit).await {
3169                Ok(attachment) => rpc_ok(
3170                    id,
3171                    serde_json::to_value(FrontendAttachSnapshot {
3172                        descriptor: attachment.descriptor,
3173                        history: attachment.history,
3174                        history_cursor: attachment.history_cursor,
3175                        replay: attachment.replay,
3176                    })
3177                    .unwrap_or_default(),
3178                ),
3179                Err(error) => sdk_runtime_rpc_error(id, -32010, &error),
3180            }
3181        }
3182        crate::FrontendFacadeMethod::SendInput => {
3183            let Some(prompt) = request.params.get("prompt").and_then(Value::as_str) else {
3184                return rpc_error(
3185                    id,
3186                    -32602,
3187                    "frontend.send_input requires a string `params.prompt`",
3188                );
3189            };
3190            let image_urls = match parse_image_urls(&request.params, "frontend.send_input") {
3191                Ok(image_urls) => image_urls,
3192                Err(message) => return rpc_error(id, -32602, message),
3193            };
3194            match runtime
3195                .clone()
3196                .send_input_with_images(prompt.to_string(), image_urls)
3197                .await
3198            {
3199                Ok(()) => rpc_ok(id, json!({"accepted": true})),
3200                Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)) => {
3201                    sdk_runtime_rpc_error(id, -32000, &error)
3202                }
3203                Err(error) => sdk_runtime_rpc_error(id, -32002, &error),
3204            }
3205        }
3206        crate::FrontendFacadeMethod::Invoke => {
3207            let operation = request
3208                .params
3209                .get("operation")
3210                .cloned()
3211                .ok_or("frontend.invoke requires `params.operation`")
3212                .and_then(|value| {
3213                    serde_json::from_value(value).map_err(|_| "invalid frontend operation")
3214                });
3215            match operation {
3216                Ok(operation) => match runtime.invoke(operation).await {
3217                    Ok(result) => rpc_ok(id, serde_json::to_value(result).unwrap_or_default()),
3218                    Err(error @ FrontendRuntimeError::UnsupportedOperation(_)) => {
3219                        sdk_runtime_rpc_error(id, -32023, &error)
3220                    }
3221                    Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)) => {
3222                        sdk_runtime_rpc_error(id, -32000, &error)
3223                    }
3224                    Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Interrupted)) => {
3225                        sdk_runtime_rpc_error(id, -32001, &error)
3226                    }
3227                    Err(error) => sdk_runtime_rpc_error(id, -32022, &error),
3228                },
3229                Err(message) => rpc_error(id, -32602, message),
3230            }
3231        }
3232        crate::FrontendFacadeMethod::Submit => {
3233            let Some(prompt) = request.params.get("prompt").and_then(Value::as_str) else {
3234                return rpc_error(id, -32602, "submit requires a string `params.prompt`");
3235            };
3236            let image_urls = match request.params.get("image_urls") {
3237                None => Vec::new(),
3238                Some(Value::Array(values)) => {
3239                    let Some(urls) = values.iter().map(Value::as_str).collect::<Option<Vec<_>>>()
3240                    else {
3241                        return rpc_error(
3242                            id,
3243                            -32602,
3244                            "submit requires string entries in `params.image_urls`",
3245                        );
3246                    };
3247                    urls.into_iter().map(str::to_owned).collect()
3248                }
3249                Some(_) => {
3250                    return rpc_error(id, -32602, "submit requires array `params.image_urls`")
3251                }
3252            };
3253            match runtime
3254                .submit_with_images(prompt.to_string(), image_urls)
3255                .await
3256            {
3257                Ok(reply) => rpc_ok(id, json!({"reply":reply})),
3258                Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)) => {
3259                    sdk_runtime_rpc_error(id, -32000, &error)
3260                }
3261                Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Interrupted)) => {
3262                    sdk_runtime_rpc_error(id, -32001, &error)
3263                }
3264                Err(error) => sdk_runtime_rpc_error(id, -32002, &error),
3265            }
3266        }
3267        crate::FrontendFacadeMethod::Interrupt => match runtime.interrupt().await {
3268            Ok(interrupted) => rpc_ok(id, json!({"interrupted":interrupted})),
3269            Err(error) => sdk_runtime_rpc_error(id, -32002, &error),
3270        },
3271        crate::FrontendFacadeMethod::Steer => {
3272            let Some(prompt) = request.params.get("prompt").and_then(Value::as_str) else {
3273                return rpc_error(id, -32602, "steer requires a string `params.prompt`");
3274            };
3275            match runtime.steer(prompt.to_string()).await {
3276                Ok(()) => rpc_ok(id, json!({"queued":true})),
3277                Err(error @ FrontendRuntimeError::UnsupportedAction(_)) => {
3278                    sdk_runtime_rpc_error(id, -32020, &error)
3279                }
3280                Err(error) => sdk_runtime_rpc_error(id, -32022, &error),
3281            }
3282        }
3283        crate::FrontendFacadeMethod::Respond => {
3284            let response = request
3285                .params
3286                .get("response")
3287                .cloned()
3288                .ok_or("respond requires `params.response`")
3289                .and_then(|value| serde_json::from_value(value).map_err(|_| "invalid response"));
3290            match response {
3291                Ok(response) => match runtime.respond(response).await {
3292                    Ok(()) => rpc_ok(id, json!({"accepted":true})),
3293                    Err(error @ FrontendRuntimeError::UnsupportedAction(_)) => {
3294                        sdk_runtime_rpc_error(id, -32020, &error)
3295                    }
3296                    Err(error) => sdk_runtime_rpc_error(id, -32022, &error),
3297                },
3298                Err(message) => rpc_error(id, -32602, message),
3299            }
3300        }
3301        crate::FrontendFacadeMethod::Lease
3302        | crate::FrontendFacadeMethod::AcquireControl
3303        | crate::FrontendFacadeMethod::TakeControl
3304        | crate::FrontendFacadeMethod::Heartbeat
3305        | crate::FrontendFacadeMethod::Detach
3306        | crate::FrontendFacadeMethod::Close => rpc_error(
3307            id,
3308            -32020,
3309            format!(
3310                "frontend action `{}` requires a coordinated runtime",
3311                method.id()
3312            ),
3313        ),
3314    }
3315}
3316
3317fn parse_image_urls(params: &Value, operation: &str) -> std::result::Result<Vec<String>, String> {
3318    let urls = match params.get("image_urls") {
3319        None => Ok(Vec::new()),
3320        Some(Value::Array(values)) => values
3321            .iter()
3322            .map(|value| {
3323                value.as_str().map(str::to_owned).ok_or_else(|| {
3324                    format!("{operation} requires string entries in `params.image_urls`")
3325                })
3326            })
3327            .collect(),
3328        Some(_) => Err(format!("{operation} requires array `params.image_urls`")),
3329    }?;
3330    validate_frontend_image_urls(urls, operation)
3331}
3332
3333fn validate_frontend_image_urls(
3334    urls: Vec<String>,
3335    operation: &str,
3336) -> std::result::Result<Vec<String>, String> {
3337    if urls.len() > 4 {
3338        return Err(format!("{operation} accepts at most 4 images"));
3339    }
3340    let mut total = 0usize;
3341    for url in &urls {
3342        if !(url.starts_with("data:image/")
3343            || url.starts_with("https://")
3344            || url.starts_with("http://"))
3345        {
3346            return Err(format!(
3347                "{operation} images must be image data URLs or HTTP(S) URLs"
3348            ));
3349        }
3350        if url.len() > 12 * 1024 * 1024 {
3351            return Err(format!("{operation} image exceeds the encoded size limit"));
3352        }
3353        total = total.saturating_add(url.len());
3354    }
3355    if total > 32 * 1024 * 1024 {
3356        return Err(format!(
3357            "{operation} images exceed the encoded total size limit"
3358        ));
3359    }
3360    Ok(urls)
3361}
3362
3363/// Lifetime handle for a frontend-only authenticated HTTP listener.
3364#[cfg(feature = "adapter-api")]
3365pub(crate) struct FrontendHttpServer {
3366    address: SocketAddr,
3367    task: tokio::task::JoinHandle<()>,
3368}
3369
3370#[cfg(feature = "adapter-api")]
3371impl FrontendHttpServer {
3372    /// Actually-bound loopback address.
3373    pub(crate) fn address(&self) -> SocketAddr {
3374        self.address
3375    }
3376}
3377
3378#[cfg(feature = "adapter-api")]
3379impl Drop for FrontendHttpServer {
3380    fn drop(&mut self) {
3381        self.task.abort();
3382    }
3383}
3384
3385/// Publish only the versioned frontend contract for a non-Agent runtime.
3386/// The caller owns the runtime and this returned listener lease.
3387#[cfg(feature = "adapter-api")]
3388pub(crate) async fn run_frontend_http(
3389    runtime: Arc<dyn FrontendRuntime>,
3390    events: broadcast::Sender<FrontendEvent>,
3391    bind: &str,
3392    token: Arc<str>,
3393    runtime_id: impl Into<String>,
3394) -> std::io::Result<FrontendHttpServer> {
3395    let listener = TcpListener::bind(bind).await?;
3396    let address = listener.local_addr()?;
3397    let coordinator = CoordinatedRuntime::new(runtime);
3398    // A REGISTRY, not a frozen list: this listener is the one the live-runtime
3399    // receipt points at, so it mints and revokes scoped frontend credentials
3400    // against its own bootstrap bearer.
3401    let credentials =
3402        RuntimeHttpCredentialRegistry::new(runtime_id, vec![RuntimeHttpCredential::owner(token)])?;
3403    let task = tokio::spawn(async move {
3404        while let Ok((stream, _)) = listener.accept().await {
3405            let coordinator = coordinator.clone();
3406            let events = events.clone();
3407            let credentials = credentials.clone();
3408            tokio::spawn(async move {
3409                let _ = handle_frontend_http_conn(stream, coordinator, events, credentials).await;
3410            });
3411        }
3412    });
3413    Ok(FrontendHttpServer { address, task })
3414}
3415
3416/// Lifetime handle for an authenticated `frontend.v2` WebSocket listener.
3417/// Dropping the handle detaches the listener without closing its SDK runtime.
3418#[cfg(feature = "adapter-api")]
3419pub struct FrontendWebSocketServer {
3420    address: SocketAddr,
3421    task: tokio::task::JoinHandle<()>,
3422}
3423
3424#[cfg(feature = "adapter-api")]
3425impl FrontendWebSocketServer {
3426    /// Actually-bound listener address.
3427    pub fn address(&self) -> SocketAddr {
3428        self.address
3429    }
3430}
3431
3432#[cfg(feature = "adapter-api")]
3433impl Drop for FrontendWebSocketServer {
3434    fn drop(&mut self) {
3435        self.task.abort();
3436    }
3437}
3438
3439/// Publish the language-neutral facade over authenticated WebSocket RPC.
3440/// The endpoint accepts only `/frontend/v2`, reuses the SDK coordinator, and
3441/// emits canonical events as `frontend.v2.event` notifications.
3442#[cfg(feature = "adapter-api")]
3443pub async fn run_frontend_websocket(
3444    engine: Arc<RpcEngine>,
3445    bind: &str,
3446    credentials: Vec<RuntimeHttpCredential>,
3447) -> std::io::Result<FrontendWebSocketServer> {
3448    let runtime: Arc<dyn FrontendRuntime> = engine.clone();
3449    let events = engine.frontend_events.clone();
3450    run_frontend_websocket_runtime_inner(runtime, events, Some(engine), bind, credentials).await
3451}
3452
3453/// Publish the authenticated WebSocket facade for any SDK runtime without
3454/// constructing an Agent or a second execution loop. The caller owns the
3455/// runtime and event sender; dropping the returned server detaches the
3456/// listener.
3457#[cfg(all(feature = "adapter-api", test))]
3458pub(crate) async fn run_frontend_websocket_runtime(
3459    runtime: Arc<dyn FrontendRuntime>,
3460    events: broadcast::Sender<FrontendEvent>,
3461    bind: &str,
3462    credentials: Vec<RuntimeHttpCredential>,
3463) -> std::io::Result<FrontendWebSocketServer> {
3464    run_frontend_websocket_runtime_inner(runtime, events, None, bind, credentials).await
3465}
3466
3467#[cfg(feature = "adapter-api")]
3468async fn run_frontend_websocket_runtime_inner(
3469    runtime: Arc<dyn FrontendRuntime>,
3470    events: broadcast::Sender<FrontendEvent>,
3471    shutdown_engine: Option<Arc<RpcEngine>>,
3472    bind: &str,
3473    credentials: Vec<RuntimeHttpCredential>,
3474) -> std::io::Result<FrontendWebSocketServer> {
3475    if credentials.is_empty()
3476        || credentials
3477            .iter()
3478            .any(|credential| credential.token.is_empty())
3479    {
3480        return Err(std::io::Error::new(
3481            std::io::ErrorKind::InvalidInput,
3482            "at least one non-empty runtime WebSocket credential is required",
3483        ));
3484    }
3485    let listener = TcpListener::bind(bind).await?;
3486    let address = listener.local_addr()?;
3487    let coordinator = CoordinatedRuntime::new(runtime);
3488    let credentials: Arc<[RuntimeHttpCredential]> = credentials.into();
3489    let task = tokio::spawn(async move {
3490        loop {
3491            tokio::select! {
3492                biased;
3493                _ = wait_for_optional_runtime_shutdown(shutdown_engine.as_ref()) => break,
3494                accepted = listener.accept() => {
3495                    let Ok((stream, _)) = accepted else { continue };
3496                    let coordinator = coordinator.clone();
3497                    let credentials = credentials.clone();
3498                    let events = events.clone();
3499                    let shutdown_engine = shutdown_engine.clone();
3500                    tokio::spawn(async move {
3501                        let _ = handle_frontend_websocket(stream, events, shutdown_engine, coordinator, credentials).await;
3502                    });
3503                }
3504            }
3505        }
3506    });
3507    Ok(FrontendWebSocketServer { address, task })
3508}
3509
3510#[cfg(feature = "adapter-api")]
3511async fn wait_for_optional_runtime_shutdown(engine: Option<&Arc<RpcEngine>>) {
3512    match engine {
3513        Some(engine) => engine.wait_for_shutdown().await,
3514        None => std::future::pending().await,
3515    }
3516}
3517
3518#[cfg(feature = "adapter-api")]
3519#[allow(clippy::result_large_err)] // tungstenite's handshake callback fixes this error type.
3520async fn handle_frontend_websocket(
3521    stream: tokio::net::TcpStream,
3522    events: broadcast::Sender<FrontendEvent>,
3523    shutdown_engine: Option<Arc<RpcEngine>>,
3524    coordinator: Arc<CoordinatedRuntime>,
3525    credentials: Arc<[RuntimeHttpCredential]>,
3526) -> Result<(), tokio_tungstenite::tungstenite::Error> {
3527    use std::sync::Mutex as SyncMutex;
3528    use tokio_tungstenite::tungstenite::handshake::server::{ErrorResponse, Request, Response};
3529
3530    let selected = Arc::new(SyncMutex::new(None::<Arc<CoordinatedRuntimeClient>>));
3531    let selected_by_callback = selected.clone();
3532    let socket = tokio_tungstenite::accept_hdr_async(
3533        stream,
3534        move |request: &Request, response: Response| -> Result<Response, ErrorResponse> {
3535            let reject = |status, message: &str| {
3536                tokio_tungstenite::tungstenite::http::Response::builder()
3537                    .status(status)
3538                    .body(Some(message.to_string()))
3539                    .expect("static WebSocket rejection is valid")
3540            };
3541            if request.uri().path() != "/frontend/v2" {
3542                return Err(reject(404, "frontend WebSocket route not found"));
3543            }
3544            let token = request
3545                .headers()
3546                .get("authorization")
3547                .and_then(|value| value.to_str().ok())
3548                .and_then(|value| value.strip_prefix("Bearer "));
3549            let Some(credential) = token.and_then(|token| {
3550                credentials.iter().find(|credential| {
3551                    constant_time_eq(token.as_bytes(), credential.token.as_bytes())
3552                })
3553            }) else {
3554                return Err(reject(401, "missing or invalid bearer token"));
3555            };
3556            let client_id = request
3557                .headers()
3558                .get("x-supercode-client-id")
3559                .and_then(|value| value.to_str().ok())
3560                .unwrap_or("legacy-websocket-owner");
3561            let Ok(client_id) = RuntimeClientId::parse(client_id) else {
3562                return Err(reject(400, "invalid runtime client id"));
3563            };
3564            let mut authorization = credential.authorization.clone();
3565            if let Some(requested) = request
3566                .headers()
3567                .get("x-supercode-permissions")
3568                .and_then(|value| value.to_str().ok())
3569            {
3570                let Ok(requested) = RuntimeAuthorization::parse_header(requested) else {
3571                    return Err(reject(400, "invalid runtime authorization grant"));
3572                };
3573                authorization = authorization.restrict_to(&requested);
3574            }
3575            *selected_by_callback
3576                .lock()
3577                .unwrap_or_else(std::sync::PoisonError::into_inner) =
3578                Some(coordinator.client(client_id, authorization));
3579            Ok(response)
3580        },
3581    )
3582    .await?;
3583    let client = selected
3584        .lock()
3585        .unwrap_or_else(std::sync::PoisonError::into_inner)
3586        .take()
3587        .expect("successful WebSocket handshake selects a runtime client");
3588    if let Err(error) = client.observe() {
3589        let mut socket = socket;
3590        let value = sdk_runtime_rpc_error(Value::Null, -32002, &error).to_string();
3591        socket
3592            .send(tokio_tungstenite::tungstenite::Message::Text(value.into()))
3593            .await?;
3594        socket.close(None).await?;
3595        return Ok(());
3596    }
3597
3598    let mut events = events.subscribe();
3599    let (mut writer, mut reader) = socket.split();
3600    loop {
3601        tokio::select! {
3602            biased;
3603            incoming = reader.next() => match incoming {
3604                Some(Ok(tokio_tungstenite::tungstenite::Message::Text(text))) => {
3605                    let response = match serde_json::from_str::<RpcRequest>(&text) {
3606                        Ok(request) => coordinated_runtime_rpc(client.clone(), request).await,
3607                        Err(error) => rpc_error(Value::Null, -32700, format!("parse error: {error}")),
3608                    };
3609                    writer.send(tokio_tungstenite::tungstenite::Message::Text(response.to_string().into())).await?;
3610                }
3611                Some(Ok(tokio_tungstenite::tungstenite::Message::Ping(payload))) => {
3612                    writer.send(tokio_tungstenite::tungstenite::Message::Pong(payload)).await?;
3613                }
3614                Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_))) | None => break,
3615                Some(Ok(_)) => {}
3616                Some(Err(error)) => {
3617                    client.detach();
3618                    return Err(error);
3619                }
3620            },
3621            event = events.recv() => match event {
3622                Ok(event) => {
3623                    let notification = json!({
3624                        "jsonrpc":"2.0",
3625                        "method":"frontend.v2.event",
3626                        "params":{"event":event},
3627                    });
3628                    writer.send(tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into())).await?;
3629                }
3630                Err(broadcast::error::RecvError::Lagged(count)) => {
3631                    let notification = json!({
3632                        "jsonrpc":"2.0",
3633                        "method":"frontend.v2.event",
3634                        "params":{"error":{"name":"transport","message":format!("event replay gap: {count}")}},
3635                    });
3636                    writer.send(tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into())).await?;
3637                    break;
3638                }
3639                Err(broadcast::error::RecvError::Closed) => break,
3640            },
3641            _ = wait_for_optional_runtime_shutdown(shutdown_engine.as_ref()) => break,
3642        }
3643    }
3644    client.detach();
3645    Ok(())
3646}
3647
3648/// Bind `bind` (`host:port`; `:0` for an OS-assigned ephemeral port) and
3649/// serve the HTTP transport (D8 "remote attach") in a background task until
3650/// `engine` signals shutdown. Returns the actually-bound address (so a
3651/// caller that asked for port `0` can learn the real port). Every
3652/// connection is authenticated per-request via `token` — see
3653/// `check_auth`. The LOOPBACK-BY-DEFAULT policy decision is the caller's
3654/// (see the module doc) — this fn binds whatever address it's given.
3655#[cfg(feature = "adapter-api")]
3656pub async fn run_http(
3657    engine: Arc<RpcEngine>,
3658    bind: &str,
3659    token: Arc<str>,
3660) -> std::io::Result<SocketAddr> {
3661    run_http_authorized(engine, bind, vec![RuntimeHttpCredential::owner(token)]).await
3662}
3663
3664/// Bind an SDK HTTP runtime with multiple independently scoped bearer
3665/// credentials. The token bytes remain server-private; each successful
3666/// authentication produces the exact authorization grant projected by the
3667/// shared runtime coordinator.
3668#[cfg(feature = "adapter-api")]
3669pub async fn run_http_authorized(
3670    engine: Arc<RpcEngine>,
3671    bind: &str,
3672    credentials: Vec<RuntimeHttpCredential>,
3673) -> std::io::Result<SocketAddr> {
3674    run_http_authorized_with_lease_ttl(
3675        engine,
3676        bind,
3677        credentials,
3678        crate::DEFAULT_RUNTIME_LEASE_TTL_MS,
3679    )
3680    .await
3681}
3682
3683/// Test/embedder variant of [`run_http_authorized`] with an explicit
3684/// controller lease duration.
3685#[cfg(feature = "adapter-api")]
3686pub async fn run_http_authorized_with_lease_ttl(
3687    engine: Arc<RpcEngine>,
3688    bind: &str,
3689    credentials: Vec<RuntimeHttpCredential>,
3690    lease_ttl_ms: u64,
3691) -> std::io::Result<SocketAddr> {
3692    if credentials.is_empty()
3693        || credentials
3694            .iter()
3695            .any(|credential| credential.token.is_empty())
3696    {
3697        return Err(std::io::Error::new(
3698            std::io::ErrorKind::InvalidInput,
3699            "at least one non-empty runtime HTTP credential is required",
3700        ));
3701    }
3702    if lease_ttl_ms == 0 {
3703        return Err(std::io::Error::new(
3704            std::io::ErrorKind::InvalidInput,
3705            "runtime lease TTL must be non-zero",
3706        ));
3707    }
3708    let listener = TcpListener::bind(bind).await?;
3709    let local_addr = listener.local_addr()?;
3710    let credentials = RuntimeHttpCredentialRegistry::new(engine.session_id(), credentials)?;
3711    let eng = engine;
3712    let runtime: Arc<dyn FrontendRuntime> = eng.clone();
3713    let coordinator = CoordinatedRuntime::with_lease_ttl(runtime, lease_ttl_ms);
3714    tokio::spawn(async move {
3715        loop {
3716            tokio::select! {
3717                biased;
3718                _ = eng.wait_for_shutdown() => break,
3719                accepted = listener.accept() => {
3720                    let Ok((stream, _addr)) = accepted else { continue };
3721                    let eng = eng.clone();
3722                    let coordinator = coordinator.clone();
3723                    let credentials = credentials.clone();
3724                    tokio::spawn(async move {
3725                        let _ = handle_http_conn(stream, eng, coordinator, credentials).await;
3726                    });
3727                }
3728            }
3729        }
3730    });
3731    Ok(local_addr)
3732}
3733
3734#[cfg(test)]
3735mod frontend_binding_conformance_tests;
3736
3737#[cfg(test)]
3738mod tests {
3739    use super::*;
3740    use tokio::io::BufReader;
3741
3742    fn cursor(data: &[u8]) -> BufReader<std::io::Cursor<Vec<u8>>> {
3743        BufReader::new(std::io::Cursor::new(data.to_vec()))
3744    }
3745
3746    #[cfg(all(feature = "adapter-api", supercode_workspace_assets))]
3747    #[test]
3748    fn packaged_observer_assets_match_the_sdk_sources() {
3749        let pairs: &[(&str, &[u8], &[u8])] = &[
3750            (
3751                "frontend-browser/index.html",
3752                include_bytes!("../embedded/frontend-browser/index.html"),
3753                include_bytes!("../../../sdk/frontend-browser/index.html"),
3754            ),
3755            (
3756                "frontend-browser/app.mjs",
3757                include_bytes!("../embedded/frontend-browser/app.mjs"),
3758                include_bytes!("../../../sdk/frontend-browser/app.mjs"),
3759            ),
3760            (
3761                "frontend-browser/client.mjs",
3762                include_bytes!("../embedded/frontend-browser/client.mjs"),
3763                include_bytes!("../../../sdk/frontend-browser/client.mjs"),
3764            ),
3765            (
3766                "frontend-browser/view.mjs",
3767                include_bytes!("../embedded/frontend-browser/view.mjs"),
3768                include_bytes!("../../../sdk/frontend-browser/view.mjs"),
3769            ),
3770            (
3771                "frontend-browser/style.css",
3772                include_bytes!("../embedded/frontend-browser/style.css"),
3773                include_bytes!("../../../sdk/frontend-browser/style.css"),
3774            ),
3775            (
3776                "frontend-browser/favicon.svg",
3777                include_bytes!("../embedded/frontend-browser/favicon.svg"),
3778                include_bytes!("../../../sdk/frontend-browser/favicon.svg"),
3779            ),
3780            (
3781                "frontend/client.mjs",
3782                include_bytes!("../embedded/frontend/client.mjs"),
3783                include_bytes!("../../../sdk/frontend/client.mjs"),
3784            ),
3785            (
3786                "frontend/generated-client.mjs",
3787                include_bytes!("../embedded/frontend/generated-client.mjs"),
3788                include_bytes!("../../../sdk/frontend/generated-client.mjs"),
3789            ),
3790            (
3791                "frontend/generated.mjs",
3792                include_bytes!("../embedded/frontend/generated.mjs"),
3793                include_bytes!("../../../sdk/frontend/generated.mjs"),
3794            ),
3795        ];
3796        for (name, packaged, source) in pairs {
3797            assert_eq!(packaged, source, "packaged observer asset drifted: {name}");
3798        }
3799    }
3800
3801    #[tokio::test]
3802    async fn admitted_submit_has_a_cancel_token_before_shutdown_observes_busy() {
3803        let agent =
3804            crate::Agent::new(crate::Config::builder().api_key("test-only-key").build()).unwrap();
3805        let engine = RpcEngine::new(agent, None);
3806        let claim = engine.claim_submit().unwrap();
3807        assert!(engine.busy.load(Ordering::SeqCst));
3808        assert!(engine
3809            .current_cancel
3810            .lock()
3811            .unwrap_or_else(std::sync::PoisonError::into_inner)
3812            .is_some());
3813
3814        let cancel = claim.cancel.clone();
3815        let shutdown_engine = engine.clone();
3816        let shutdown = tokio::spawn(async move { shutdown_engine.shutdown().await });
3817        tokio::time::timeout(std::time::Duration::from_secs(1), cancel.notified())
3818            .await
3819            .expect("shutdown must interrupt an admitted claim before its future starts");
3820        assert!(
3821            !shutdown.is_finished(),
3822            "shutdown must retain the barrier until the admitted claim drains"
3823        );
3824        drop(claim);
3825        tokio::time::timeout(std::time::Duration::from_secs(1), shutdown)
3826            .await
3827            .expect("claim drain must release shutdown")
3828            .unwrap();
3829    }
3830
3831    #[tokio::test]
3832    async fn read_bounded_line_reads_a_normal_line() {
3833        let mut r = cursor(b"hello\nworld\n");
3834        assert_eq!(
3835            read_bounded_line(&mut r, 1024).await.unwrap(),
3836            Some("hello".to_string())
3837        );
3838        assert_eq!(
3839            read_bounded_line(&mut r, 1024).await.unwrap(),
3840            Some("world".to_string())
3841        );
3842        assert_eq!(read_bounded_line(&mut r, 1024).await.unwrap(), None);
3843    }
3844
3845    #[tokio::test]
3846    async fn read_bounded_line_strips_trailing_cr() {
3847        let mut r = cursor(b"hello\r\n");
3848        assert_eq!(
3849            read_bounded_line(&mut r, 1024).await.unwrap(),
3850            Some("hello".to_string())
3851        );
3852    }
3853
3854    #[tokio::test]
3855    async fn read_bounded_line_returns_final_line_without_trailing_newline() {
3856        let mut r = cursor(b"no newline at eof");
3857        assert_eq!(
3858            read_bounded_line(&mut r, 1024).await.unwrap(),
3859            Some("no newline at eof".to_string())
3860        );
3861        assert_eq!(read_bounded_line(&mut r, 1024).await.unwrap(), None);
3862    }
3863
3864    #[tokio::test]
3865    async fn read_bounded_line_errors_and_resyncs_on_an_oversized_line() {
3866        let mut data = vec![b'x'; 20];
3867        data.push(b'\n');
3868        data.extend_from_slice(b"next\n");
3869        let mut r = cursor(&data);
3870        let err = read_bounded_line(&mut r, 10).await.unwrap_err();
3871        assert!(err.to_string().contains("10 byte cap"));
3872        // Resynced: the NEXT call sees the following real line, not more
3873        // of the oversized one.
3874        assert_eq!(
3875            read_bounded_line(&mut r, 1024).await.unwrap(),
3876            Some("next".to_string())
3877        );
3878    }
3879
3880    #[test]
3881    fn constant_time_eq_matches_equal_slices() {
3882        assert!(constant_time_eq(b"abc123", b"abc123"));
3883    }
3884
3885    #[test]
3886    fn constant_time_eq_rejects_different_length_or_content() {
3887        assert!(!constant_time_eq(b"abc123", b"abc1234"));
3888        assert!(!constant_time_eq(b"abc123", b"xbc123"));
3889    }
3890
3891    #[test]
3892    fn generate_token_is_64_hex_chars_and_varies() {
3893        let a = generate_token();
3894        let b = generate_token();
3895        assert_eq!(a.len(), 64);
3896        assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
3897        assert_ne!(a, b, "two calls must not mint the same token");
3898    }
3899
3900    #[cfg(feature = "adapter-api")]
3901    #[tokio::test]
3902    async fn credential_revocation_waits_for_registered_attachment_ack() {
3903        let revocation = Arc::new(RuntimeCredentialRevocation::new());
3904        let attachment = revocation.register();
3905        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
3906        let task = tokio::spawn({
3907            let revocation = revocation.clone();
3908            async move {
3909                let _ = started_tx.send(());
3910                revocation.revoke_and_wait().await;
3911            }
3912        });
3913
3914        started_rx.await.unwrap();
3915        tokio::task::yield_now().await;
3916        assert!(
3917            !task.is_finished(),
3918            "revoke must remain pending while the attachment is registered"
3919        );
3920
3921        drop(attachment);
3922        tokio::time::timeout(std::time::Duration::from_secs(1), task)
3923            .await
3924            .expect("attachment acknowledgement must release revoke")
3925            .unwrap();
3926    }
3927
3928    #[cfg(feature = "adapter-api")]
3929    #[tokio::test]
3930    async fn credential_revocation_has_no_check_to_wait_lost_wakeup() {
3931        for _ in 0..10_000 {
3932            let revocation = Arc::new(RuntimeCredentialRevocation::new());
3933            let attachment = revocation.register();
3934            let (started_tx, started_rx) = tokio::sync::oneshot::channel();
3935            let task = tokio::spawn({
3936                let revocation = revocation.clone();
3937                async move {
3938                    let _ = started_tx.send(());
3939                    revocation.revoke_and_wait().await;
3940                }
3941            });
3942
3943            started_rx.await.unwrap();
3944            drop(attachment);
3945            tokio::time::timeout(std::time::Duration::from_secs(1), task)
3946                .await
3947                .expect("revoke lost its attachment-drained wakeup")
3948                .unwrap();
3949        }
3950    }
3951
3952    #[test]
3953    fn request_history_compaction_deduplicates_and_orders_resolutions() {
3954        let request = |sequence, id| {
3955            FrontendEvent::new(
3956                sequence,
3957                json!({"type": "request", "request": {"id": id, "kind": "approval", "payload": {}}}),
3958            )
3959        };
3960        let resolved = |sequence, id| {
3961            FrontendEvent::new(
3962                sequence,
3963                json!({"type": "request_resolved", "request_id": id, "response": {"kind": "approval", "request_id": id, "decision": "allow"}}),
3964            )
3965        };
3966        let replay = VecDeque::from([
3967            request(1, 2),
3968            resolved(2, 2),
3969            request(3, 1),
3970            resolved(4, 1),
3971            request(5, 2),
3972            resolved(6, 1),
3973        ]);
3974
3975        let compacted = compact_frontend_request_history(&replay);
3976        assert_eq!(compacted.len(), 4);
3977        assert_eq!(compacted[0]["request"]["id"], 1);
3978        assert_eq!(compacted[1]["request_id"], 1);
3979        assert_eq!(compacted[2]["request"]["id"], 2);
3980        assert_eq!(compacted[3]["request_id"], 2);
3981    }
3982}