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 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::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, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct Seat {
#[serde(skip)]
pub path: PathBuf,
pub harness: Harness,
pub door_socket: PathBuf,
pub serve_config: PathBuf,
pub key_file: PathBuf,
pub issuer: String,
pub store: String,
}
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 mut decoder = serde_json::Deserializer::from_slice(&bytes);
let mut seat: Self = 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()
)
})?;
seat.path = std::fs::canonicalize(path)?;
seat.harness.validate()?;
super::environment::check_provider(&seat.harness.environment)?;
anyhow::ensure!(
seat.door_socket.is_absolute()
&& seat.serve_config.is_absolute()
&& seat.key_file.is_absolute(),
"seat_paths_must_be_absolute"
);
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
}