liminal-cli 0.3.4

The liminal command: serve the network, maintain its stores, or serve its discovery MCP over stdio
Documentation
//! A protocol peer with a separate signal pipe; no provider or exchange is simulated.

use clap::Parser;
use serde_json::{Value, json};
use std::collections::BTreeSet;
use std::io::{BufRead, BufReader, Read as _, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::{Arc, Mutex};

#[derive(Parser)]
struct Args {
    #[arg(long)]
    control: PathBuf,
    #[arg(long, default_value_t = 1)]
    version: u16,
    #[arg(long)]
    no_load: bool,
    #[arg(long)]
    hold: bool,
    #[arg(long, default_value_t = 0)]
    burst: usize,
    #[arg(long)]
    exit: Option<u8>,
    /// The fixture's configured frame bound keeps either test pipe finite.
    #[arg(long, default_value_t = 262_144)]
    frame_bytes: usize,
}

struct State {
    prompt: Option<Value>,
    turn: u64,
    answered: BTreeSet<ValueKey>,
    stdout: std::io::Stdout,
    control: UnixStream,
    version: u16,
    bound: usize,
}

#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct ValueKey(String);

impl State {
    fn wire(&mut self, value: &Value) -> anyhow::Result<()> {
        line(&mut self.stdout, value, self.bound)
    }
    fn signal(&mut self, value: &Value) -> anyhow::Result<()> {
        line(&mut self.control, value, self.bound)
    }
    fn update(&mut self, update: &Value) -> anyhow::Result<()> {
        self.wire(&json!({"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"fake-session","update":update}}))
    }
    fn finish(&mut self, reason: &str) -> anyhow::Result<()> {
        if let Some(id) = self.prompt.take() {
            if self.version == 2 {
                self.update(
                    &json!({"sessionUpdate":"state_update","state":"idle","stopReason":reason}),
                )?;
            } else {
                self.wire(&json!({"jsonrpc":"2.0","id":id,"result":{"stopReason":reason}}))?;
            }
            self.signal(&json!({"event":"turn_ended","turn":self.turn}))?;
        }
        Ok(())
    }
}

/// The models the fake offers; any other value is refused.
const OFFERED_MODELS: [&str; 2] = ["fake-small", "fake-large"];

fn options(current: &Value) -> Value {
    json!([{"id":"model","name":"Model","type":"select","currentValue":current,
        "options":OFFERED_MODELS.iter().map(|model| json!({"value":model,"name":model})).collect::<Vec<_>>()}])
}

fn line<W: Write>(writer: &mut W, value: &Value, bound: usize) -> anyhow::Result<()> {
    let mut bytes = serde_json::to_vec(value)?;
    bytes.push(b'\n');
    anyhow::ensure!(bytes.len() <= bound, "fake output frame bound exceeded");
    writer.write_all(&bytes)?;
    writer.flush()?;
    Ok(())
}

fn read<R: BufRead>(reader: &mut R, bound: usize) -> anyhow::Result<Option<Value>> {
    let mut bytes = Vec::new();
    let count = reader
        .take(u64::try_from(bound)?.saturating_add(1))
        .read_until(b'\n', &mut bytes)?;
    if count == 0 {
        return Ok(None);
    }
    anyhow::ensure!(
        count <= bound && bytes.last() == Some(&b'\n'),
        "fake input frame bound or newline refused"
    );
    Ok(Some(
        serde_json::from_slice(&bytes).map_err(|_| anyhow::anyhow!("fake input is not JSON"))?,
    ))
}

fn run(args: &Args) -> anyhow::Result<u8> {
    anyhow::ensure!(
        args.frame_bytes > 0 && args.burst <= args.frame_bytes,
        "fake bounds refused"
    );
    let control = UnixStream::connect(&args.control)?;
    let control_reader = control.try_clone()?;
    let state = Arc::new(Mutex::new(State {
        prompt: None,
        turn: 0,
        answered: BTreeSet::new(),
        stdout: std::io::stdout(),
        control,
        version: args.version,
        bound: args.frame_bytes,
    }));
    let controls = Arc::clone(&state);
    let bound = args.frame_bytes;
    std::thread::spawn(move || {
        let mut reader = BufReader::new(control_reader);
        let result: anyhow::Result<()> = (|| {
            while let Some(command) = read(&mut reader, bound)? {
                let mut state = controls
                    .lock()
                    .map_err(|_| anyhow::anyhow!("fake control lock poisoned"))?;
                match command["command"].as_str() {
                    Some("finish") => state.finish("end_turn")?,
                    Some("exit") => {
                        state.signal(&json!({"event":"exit_requested"}))?;
                        std::process::exit(3);
                    }
                    _ => anyhow::bail!("fake control command refused"),
                }
                drop(state);
            }
            Ok(())
        })();
        if result.is_err() {
            eprintln!("fake ACP control failed");
        }
    });
    let stdin = std::io::stdin();
    let mut reader = stdin.lock();
    while let Some(message) = read(&mut reader, bound)? {
        let mut state = state
            .lock()
            .map_err(|_| anyhow::anyhow!("fake state lock poisoned"))?;
        match message["method"].as_str() {
            Some("initialize") => {
                anyhow::ensure!(
                    message["params"]["protocolVersion"] == 2,
                    "host did not offer ACP 2"
                );
                state.signal(&json!({"event":"initialize","offered":2}))?;
                let result = if args.version == 2 {
                    json!({"protocolVersion":2,"info":{"name":"fake","version":"1"},"capabilities":{"session":{}}})
                } else {
                    json!({"protocolVersion":args.version,"agentCapabilities":{"loadSession":!args.no_load},"authMethods":[]})
                };
                state.wire(&json!({"jsonrpc":"2.0","id":message["id"],"result":result}))?;
            }
            Some(method @ ("session/new" | "session/load" | "session/resume")) => {
                anyhow::ensure!(
                    message["params"]["mcpServers"][0]["name"] == "liminal",
                    "fake received no ticket"
                );
                state.signal(&json!({"event":"session_opened","method":method}))?;
                let result = if method == "session/new" {
                    json!({"sessionId":"fake-session","configOptions":options(&json!(OFFERED_MODELS[0]))})
                } else {
                    json!({})
                };
                state.wire(&json!({"jsonrpc":"2.0","id":message["id"],"result":result}))?;
            }
            Some("session/list") => listed(&mut state, &message)?,
            Some("session/set_config_option") => configured(&mut state, &message)?,
            Some("session/prompt") => {
                if let Some(code) = prompt(&mut state, args, &message)? {
                    return Ok(code);
                }
            }
            Some("session/cancel") => {
                state.signal(&json!({"event":"cancel"}))?;
                state.finish("cancelled")?;
            }
            Some(_) => anyhow::bail!("fake received an unknown method"),
            None => {
                let id = ValueKey(serde_json::to_string(&message["id"])?);
                anyhow::ensure!(
                    state.answered.insert(id),
                    "fake received a duplicate permission answer"
                );
                let answered = state.answered.len();
                state.signal(&json!({"event":"permission_answer","count":answered,"outcome":message["result"]["outcome"]}))?;
                if answered == args.burst {
                    state.finish("end_turn")?;
                }
            }
        }
    }
    Ok(0)
}

/// The harness's own conversations, newest last as a harness keeps them.
fn listed(state: &mut State, message: &Value) -> anyhow::Result<()> {
    state.signal(&json!({"event":"session_list"}))?;
    let sessions = json!([
        {"sessionId":"fake-earlier","title":"Earlier conversation","updatedAt":"2026-10-15T09:00:00Z"},
        {"sessionId":"fake-session","title":"This conversation","updatedAt":"2026-10-16T09:00:00Z"},
    ]);
    state.wire(&json!({"jsonrpc":"2.0","id":message["id"],"result":{"sessions":sessions}}))?;
    Ok(())
}

/// One option set when the fake offers the value.
fn configured(state: &mut State, message: &Value) -> anyhow::Result<()> {
    let value = &message["params"]["value"];
    state.signal(
        &json!({"event":"config_set","configId":message["params"]["configId"],"value":value}),
    )?;
    // A value the fake does not offer is refused as ACP's invalid params, as a harness would.
    let answer = if message["params"]["configId"] == "model"
        && OFFERED_MODELS.iter().any(|model| value == model)
    {
        json!({"jsonrpc":"2.0","id":message["id"],"result":{"configOptions":options(value)}})
    } else {
        json!({"jsonrpc":"2.0","id":message["id"],"error":{"code":-32602,"message":"fake offers no such value"}})
    };
    state.wire(&answer)?;
    Ok(())
}

fn prompt(state: &mut State, args: &Args, message: &Value) -> anyhow::Result<Option<u8>> {
    anyhow::ensure!(state.prompt.is_none(), "host sent two simultaneous prompts");
    anyhow::ensure!(
        message["params"]["sessionId"] == "fake-session",
        "prompt names a foreign session"
    );
    state.turn += 1;
    state.prompt = Some(message["id"].clone());
    let turn = state.turn;
    state.signal(
        &json!({"event":"prompt","turn":turn,"text":message["params"]["prompt"][0]["text"]}),
    )?;
    if args.version == 2 {
        state.update(&json!({"sessionUpdate":"state_update","state":"running"}))?;
        state.wire(&json!({"jsonrpc":"2.0","id":message["id"],"result":{}}))?;
    }
    let tool = if args.version == 2 {
        "tool_call_update"
    } else {
        "tool_call"
    };
    state.update(&json!({"sessionUpdate":tool,"toolCallId":"tool","title":"Synthetic review","status":"in_progress","rawInput":{"private":"input"},"rawOutput":{"private":"output"},"content":[{"type":"content","content":{"type":"text","text":"file contents"}}]}))?;
    state.update(&json!({"sessionUpdate":"agent_message_chunk","messageId":format!("message-{}", state.turn),"content":{"type":"text","text":"Synthetic answer"}}))?;
    if let Some(code) = args.exit {
        return Ok(Some(code));
    }
    for id in 0..args.burst {
        let options = json!([{"optionId":"yes","name":"Approve","kind":"allow_once"},{"optionId":"no","name":"Refuse","kind":"reject_once"}]);
        let params = if args.version == 2 {
            json!({"sessionId":"fake-session","title":"Synthetic review","options":options})
        } else {
            json!({"sessionId":"fake-session","toolCall":{"toolCallId":format!("tool-{id}"),"title":"Synthetic review"},"options":options})
        };
        state.wire(&json!({"jsonrpc":"2.0","id":format!("permission-{id}"),"method":"session/request_permission","params":params}))?;
    }
    if args.burst == 0 && !args.hold {
        state.finish("end_turn")?;
    }
    Ok(None)
}

fn main() -> ExitCode {
    run(&Args::parse()).map_or_else(
        |_| {
            eprintln!("fake ACP protocol failed");
            ExitCode::from(1)
        },
        ExitCode::from,
    )
}