use serde::Deserialize;
use std::path::{Path, PathBuf};
#[derive(Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Harness {
pub id: String,
pub program: PathBuf,
pub args: Vec<String>,
#[serde(default)]
pub home: PathBuf,
#[serde(default)]
pub environment: Vec<String>,
#[serde(default = "reads")]
pub read_queue: usize,
#[serde(default = "deadline_ms")]
pub initialize_timeout_ms: u64,
#[serde(default = "deadline_ms")]
pub session_timeout_ms: u64,
pub cwd: Option<PathBuf>,
pub session: Option<String>,
#[serde(default = "prompt_queue")]
pub prompt_queue: usize,
#[serde(default = "content_bytes")]
pub content_bytes: usize,
#[serde(default = "frame_bytes")]
pub acp_frame_bytes: usize,
#[serde(default = "permissions")]
pub pending_permissions: usize,
#[serde(default = "writes")]
pub write_queue: usize,
#[serde(default = "streams")]
pub stream_queue: usize,
#[serde(default = "content_bytes")]
pub issuer_response_bytes: usize,
#[serde(default = "sessions")]
pub session_list: usize,
#[serde(default = "titles")]
pub title_chars: usize,
}
const fn sessions() -> usize {
50
}
const fn titles() -> usize {
120
}
const fn prompt_queue() -> usize {
32
}
const fn content_bytes() -> usize {
65_536
}
const fn frame_bytes() -> usize {
262_144
}
const fn permissions() -> usize {
1024
}
const fn writes() -> usize {
2048
}
const fn streams() -> usize {
128
}
impl Harness {
pub fn validate(&self) -> anyhow::Result<()> {
super::catalogue::validate(&self.id)?;
super::environment::home(&self.home)?;
super::environment::validate(&self.environment)?;
anyhow::ensure!(
self.initialize_timeout_ms > 0,
"harness.initialize_timeout_ms must be positive"
);
anyhow::ensure!(
self.session_timeout_ms > 0,
"harness.session_timeout_ms must be positive"
);
anyhow::ensure!(
!self.program.as_os_str().is_empty(),
"harness.program is empty"
);
anyhow::ensure!(
self.cwd.as_ref().is_none_or(|cwd| cwd.is_absolute()),
"harness.cwd must be absolute"
);
anyhow::ensure!(
self.session
.as_ref()
.is_none_or(|id| !id.trim().is_empty() && id.len() <= self.content_bytes),
"harness.session is empty or exceeds content_bytes"
);
for (name, bound) in [
("session_list", self.session_list),
("title_chars", self.title_chars),
("prompt_queue", self.prompt_queue),
("content_bytes", self.content_bytes),
("acp_frame_bytes", self.acp_frame_bytes),
("pending_permissions", self.pending_permissions),
("read_queue", self.read_queue),
("write_queue", self.write_queue),
("stream_queue", self.stream_queue),
("issuer_response_bytes", self.issuer_response_bytes),
] {
anyhow::ensure!(bound > 0, "harness.{name} must be positive");
}
for (name, bound) in [
("read_queue", self.read_queue),
("write_queue", self.write_queue),
("stream_queue", self.stream_queue),
] {
anyhow::ensure!(
bound <= tokio::sync::Semaphore::MAX_PERMITS,
"harness.{name} exceeds channel capacity"
);
}
Ok(())
}
}
#[derive(Clone)]
pub(super) enum Place {
Local {
door_socket: PathBuf,
serve_config: PathBuf,
},
Network {
door_url: url::Url,
store: StoreEndpoint,
},
}
#[derive(Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct StoreEndpoint {
pub endpoint: String,
#[serde(default)]
pub server_name: String,
pub max_page: Option<std::num::NonZeroU32>,
pub timeout_ms: Option<std::num::NonZeroU64>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct SeatFile {
harness: Harness,
door_socket: Option<PathBuf>,
serve_config: Option<PathBuf>,
door_url: Option<String>,
store_endpoint: Option<StoreEndpoint>,
issuer: String,
store: String,
}
#[derive(Clone, Deserialize)]
#[serde(try_from = "SeatFile")]
pub(super) struct Seat {
pub path: PathBuf,
pub harness: Harness,
pub place: Place,
pub issuer: String,
pub store: String,
}
impl TryFrom<SeatFile> for Seat {
type Error = String;
fn try_from(file: SeatFile) -> Result<Self, String> {
let place = place(
file.door_socket,
file.serve_config,
file.door_url,
file.store_endpoint,
)
.map_err(|error| error.to_string())?;
Ok(Self {
path: PathBuf::new(),
harness: file.harness,
place,
issuer: file.issuer,
store: file.store,
})
}
}
impl Seat {
pub fn load(path: &Path) -> anyhow::Result<Self> {
let bytes = std::fs::read(path)
.map_err(|error| anyhow::anyhow!("seat_unreadable: {}", error.kind()))?;
let fields: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|_| anyhow::anyhow!("seat_invalid: expected a JSON seat object"))?;
anyhow::ensure!(
fields.get("key_file").is_none(),
"seat_invalid: key_file is forbidden; the run supplies its launch proof"
);
let mut decoder = serde_json::Deserializer::from_slice(&bytes);
let file: SeatFile = serde_path_to_error::deserialize(&mut decoder).map_err(|error| {
let field: String = error
.path()
.to_string()
.chars()
.take(160)
.filter(|character| {
character.is_ascii_alphanumeric() || ".[]_".contains(*character)
})
.collect();
anyhow::anyhow!(
"seat_invalid: field {field}, line {}, column {}, {:?}",
error.inner().line(),
error.inner().column(),
error.inner().classify()
)
})?;
decoder.end().map_err(|error| {
anyhow::anyhow!(
"seat_invalid: trailing input, line {}, column {}",
error.line(),
error.column()
)
})?;
let mut seat = Self::try_from(file).map_err(anyhow::Error::msg)?;
seat.path = std::fs::canonicalize(path)?;
seat.harness.validate()?;
super::environment::check_provider(&seat.harness.environment)?;
anyhow::ensure!(
!seat.store.is_empty() && !seat.issuer.is_empty(),
"seat_store_and_issuer_required"
);
Ok(seat)
}
pub fn cwd(&self) -> anyhow::Result<PathBuf> {
self.harness
.cwd
.clone()
.map_or_else(|| Ok(std::env::current_dir()?), Ok)
}
}
const fn reads() -> usize {
1
}
const fn deadline_ms() -> u64 {
30_000
}
fn place(
door_socket: Option<PathBuf>,
serve_config: Option<PathBuf>,
door_url: Option<String>,
store_endpoint: Option<StoreEndpoint>,
) -> anyhow::Result<Place> {
let local = door_socket.is_some() || serve_config.is_some();
let network = door_url.is_some() || store_endpoint.is_some();
anyhow::ensure!(
!(local && network),
"seat_place_mixed: give door_socket and serve_config, or door_url and store_endpoint, never parts of both"
);
match (door_socket, serve_config, door_url, store_endpoint) {
(Some(door_socket), Some(serve_config), None, None) => {
anyhow::ensure!(
door_socket.is_absolute() && serve_config.is_absolute(),
"seat_paths_must_be_absolute"
);
Ok(Place::Local {
door_socket,
serve_config,
})
}
(None, None, Some(door_url), Some(store)) => {
let door_url = door_origin(&door_url)?;
anyhow::ensure!(
crate::client::network::endpoint_parses(&store.endpoint),
"seat_store_endpoint_invalid: expected <host>:<port>"
);
anyhow::ensure!(
!store.server_name.is_empty(),
"seat_store_endpoint_requires_server_name"
);
crate::client::network::name(&store.server_name)
.map_err(|_| anyhow::anyhow!("seat_store_server_name_invalid"))?;
Ok(Place::Network { door_url, store })
}
_ => anyhow::bail!(
"seat_place_incomplete: give door_socket and serve_config, or door_url and store_endpoint"
),
}
}
fn door_origin(text: &str) -> anyhow::Result<url::Url> {
let url = url::Url::parse(text).map_err(|_| anyhow::anyhow!("seat_door_url_invalid"))?;
anyhow::ensure!(url.scheme() == "https", "seat_door_url_requires_https");
anyhow::ensure!(
url.host_str().is_some_and(|host| !host.is_empty())
&& url.username().is_empty()
&& url.password().is_none()
&& url.path() == "/"
&& url.query().is_none()
&& url.fragment().is_none(),
"seat_door_url_invalid: expected the door's origin, https://<host>[:<port>]"
);
Ok(url)
}