basis 0.3.0

The basis SDK: workspace discovery, run lifecycle, one event stream, and the two seams. No protocol, no transport, no TTY.
Documentation
//! What building a runtime settles, and what it must not.
//!
//! Split out of `builder.rs` for its size, the same ruling as
//! `workspace/builder/tests.rs` — where most of these lived before ADR-0018
//! moved the knobs they exercise.

use std::borrow::Cow;
use std::collections::BTreeMap;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;

use super::*;

fn read_http_request(stream: &mut TcpStream) -> String {
    let mut bytes = Vec::new();
    let mut buffer = [0_u8; 4096];
    let mut header_end = None;
    let mut content_length = 0_usize;

    loop {
        let read = stream.read(&mut buffer).expect("read request");
        if read == 0 {
            break;
        }
        bytes.extend_from_slice(&buffer[..read]);
        if header_end.is_none()
            && let Some(index) = bytes.windows(4).position(|window| window == b"\r\n\r\n")
        {
            let end = index + 4;
            header_end = Some(end);
            let headers = String::from_utf8_lossy(&bytes[..end]);
            content_length = headers
                .lines()
                .find_map(|line| {
                    let (name, value) = line.split_once(':')?;
                    name.eq_ignore_ascii_case("content-length")
                        .then(|| value.trim().parse::<usize>().expect("content length"))
                })
                .unwrap_or_default();
        }
        if header_end.is_some_and(|end| bytes.len() >= end + content_length) {
            break;
        }
    }

    String::from_utf8(bytes).expect("request should be utf8")
}

fn spawn_two_response_server() -> (String, thread::JoinHandle<Vec<String>>) {
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server");
    let address = listener.local_addr().expect("read server address");
    let handle = thread::spawn(move || {
        let mut requests = Vec::new();
        for index in 1..=2 {
            let (mut stream, _) = listener.accept().expect("accept request");
            requests.push(read_http_request(&mut stream));
            let response_id = format!("resp_{index}");
            let body = format!(
                concat!(
                    "data: {{\"type\":\"response.created\",\"response\":{{\"id\":\"{}\",\"model\":\"gpt-5\",\"status\":\"in_progress\"}}}}\n\n",
                    "data: {{\"type\":\"response.completed\",\"response\":{{\"id\":\"{}\",\"model\":\"gpt-5\",\"status\":\"completed\"}}}}\n\n"
                ),
                response_id, response_id
            );
            let response = format!(
                concat!(
                    "HTTP/1.1 200 OK\r\n",
                    "connection: close\r\n",
                    "content-type: text/event-stream\r\n",
                    "content-length: {}\r\n\r\n",
                    "{}"
                ),
                body.len(),
                body
            );
            stream
                .write_all(response.as_bytes())
                .expect("write response");
        }
        requests
    });

    (format!("http://{address}/"), handle)
}

#[tokio::test]
async fn compatible_provider_skips_automatic_previous_response_id_chaining() {
    let (base_url, handle) = spawn_two_response_server();
    let provider = compatible_provider(&base_url, "test-key");

    for (index, message) in ["first", "second"].into_iter().enumerate() {
        let request = mentra::provider_core::Request {
            model: Cow::Borrowed("gpt-5"),
            system: None,
            messages: Cow::Owned(vec![mentra::Message::user(mentra::ContentBlock::text(
                message,
            ))]),
            tools: Cow::Owned(Vec::new()),
            tool_choice: None,
            temperature: None,
            max_output_tokens: None,
            metadata: Cow::Owned(BTreeMap::new()),
            provider_request_options: Default::default(),
        };
        let mut stream = provider
            .session()
            .stream_response(request)
            .await
            .expect("compatible provider should stream");
        while let Some(event) = stream.recv().await {
            event.expect("response event should decode");
        }
        if index == 0 {
            assert_eq!(
                provider.session().latest_response_id().as_deref(),
                Some("resp_1"),
                "the second request must have provider state available to suppress"
            );
        }
    }

    let requests = handle.join().expect("server should capture requests");
    for request in requests {
        let body = request.split("\r\n\r\n").nth(1).unwrap_or_default();
        let payload: serde_json::Value =
            serde_json::from_str(body).expect("request body should be json");
        assert!(payload.get("previous_response_id").is_none());
    }
}

#[test]
fn builders_return_new_values() {
    let base = RuntimeBuilder::default();
    let derived = base.with_provider(BuiltinProvider::Anthropic);

    assert_eq!(derived.provider, Some(BuiltinProvider::Anthropic));
    assert_eq!(
        RuntimeBuilder::default().provider,
        None,
        "a fresh builder detects the provider"
    );
}

#[test]
fn history_goes_where_mentra_puts_it_unless_the_caller_says_otherwise() {
    // The default must stay the default: a host with conversations already
    // in mentra's database would lose sight of them if building a runtime
    // started relocating the store on its own.
    assert_eq!(RuntimeBuilder::default().history, None);
    assert_eq!(
        RuntimeBuilder::default()
            .with_store_dir("/elsewhere")
            .history,
        Some(History::Directory(PathBuf::from("/elsewhere")))
    );
    assert_eq!(
        RuntimeBuilder::default().with_ephemeral_history().history,
        Some(History::Ephemeral)
    );
}

#[test]
fn the_last_word_about_history_is_the_one_that_counts() {
    // The two knobs answer one question, so they write one field and neither
    // can be left half in force. A helper that hands out ephemeral builders
    // has to be overridable by the caller that wants its history kept, and
    // the reverse has to work as plainly.
    assert_eq!(
        RuntimeBuilder::default()
            .with_store_dir("/elsewhere")
            .with_ephemeral_history()
            .history,
        Some(History::Ephemeral)
    );
    assert_eq!(
        RuntimeBuilder::default()
            .with_ephemeral_history()
            .with_store_dir("/elsewhere")
            .history,
        Some(History::Directory(PathBuf::from("/elsewhere")))
    );
}

#[test]
fn a_supplied_credential_is_not_printed() {
    let printed = format!(
        "{:?}",
        RuntimeBuilder::default().with_api_key("sk-secret-value")
    );

    assert!(!printed.contains("sk-secret-value"));
    assert!(printed.contains("redacted"));
}

#[test]
fn command_environment_is_scoped_and_redacted() {
    let builder = RuntimeBuilder::default()
        .with_command_environment("BASIS_TASK_ID", "parent")
        .with_command_environment("BASIS_TASK_ID", "child");

    assert_eq!(
        builder.command_environment.get("BASIS_TASK_ID"),
        Some(&"child".to_string()),
        "the last fixed value is the only value a command should receive"
    );
    let printed = format!("{builder:?}");
    assert!(printed.contains("BASIS_TASK_ID"), "{printed}");
    assert!(!printed.contains("child"), "{printed}");
}

struct Named(&'static str);

#[async_trait::async_trait]
impl Interceptor for Named {
    fn name(&self) -> &str {
        self.0
    }

    async fn intercept(
        &self,
        _call: &crate::HookRequest,
    ) -> Result<crate::HookOutcome, crate::InterceptorError> {
        Ok(crate::HookOutcome::Allow)
    }
}

#[test]
fn interceptors_append_in_the_order_they_were_registered() {
    // Ordering is the whole of what registration decides — the first
    // refusal short-circuits — so it has to be the caller's, not a set's.
    let builder = RuntimeBuilder::default()
        .with_interceptor(Named("first"))
        .with_interceptor(Named("second"));

    assert_eq!(
        builder
            .interceptors
            .iter()
            .map(|interceptor| interceptor.name())
            .collect::<Vec<_>>(),
        vec!["first", "second"]
    );
    assert!(
        RuntimeBuilder::default().interceptors.is_empty(),
        "a fresh builder intercepts nothing"
    );
}

#[test]
fn a_registered_interceptor_is_named_in_the_debug_view() {
    // The Debug impl is hand-written to keep a credential out of a log, so
    // each new field has to be added to it deliberately.
    let printed = format!(
        "{:?}",
        RuntimeBuilder::default().with_interceptor(Named("redact"))
    );

    assert!(printed.contains("redact"), "{printed}");
}

#[test]
fn the_shared_policy_grants_commands_with_workspace_bounded_patience() {
    // `RuntimePolicy` exposes no getters, so the pin reads its derived Debug
    // — brittle against a mentra rename, but a rename would fail loudly here
    // rather than silently changing what a shared runtime may do.
    let printed = format!("{:?}", shared_policy());

    assert!(printed.contains("allow_shell_commands: true"), "{printed}");
    assert!(
        printed.contains("allow_background_commands: true"),
        "{printed}"
    );
    assert!(
        printed.contains("default_command_timeout: 120s"),
        "{printed}"
    );
    assert!(printed.contains("max_command_timeout: 600s"), "{printed}");
    assert!(
        printed.contains("allowed_read_roots: []") && printed.contains("allowed_write_roots: []"),
        "no roots: each agent is confined to its own base_dir, and no \
         workspace's root may widen another's: {printed}"
    );
}

#[test]
fn a_shared_runtime_resolves_its_provider_without_the_network() {
    // `build` is sync, so everything it does must be local: credential
    // lookup, assembly, nothing else. A closed port proves nothing is
    // contacted — reaching it would error, not hang.
    let runtime = RuntimeBuilder::default()
        .with_base_url("http://127.0.0.1:1/v1")
        .with_api_key("test-key")
        .with_ephemeral_history()
        .build()
        .expect("builds offline");

    assert_eq!(runtime.provider(), "openai");
}