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