use crate::{Config, Credential, Limits};
use serde::Deserialize;
use std::path::{Path, PathBuf};
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Harness {
pub id: String,
pub program: PathBuf,
pub args: Vec<String>,
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 = "read_queue")]
pub read_queue: usize,
#[serde(default = "streams")]
pub stream_queue: usize,
#[serde(default = "content_bytes")]
pub issuer_response_bytes: usize,
#[serde(default = "listed_sessions")]
pub listed_sessions: usize,
#[serde(default = "title_chars")]
pub title_chars: usize,
}
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
}
const fn read_queue() -> usize {
1
}
const fn listed_sessions() -> usize {
50
}
const fn title_chars() -> usize {
120
}
impl Harness {
pub fn validate(&self) -> anyhow::Result<()> {
super::catalogue::validate(&self.id)?;
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 [
("prompt_queue", self.prompt_queue),
("content_bytes", self.content_bytes),
("acp_frame_bytes", self.acp_frame_bytes),
("pending_permissions", self.pending_permissions),
("write_queue", self.write_queue),
("read_queue", self.read_queue),
("stream_queue", self.stream_queue),
("issuer_response_bytes", self.issuer_response_bytes),
("listed_sessions", self.listed_sessions),
("title_chars", self.title_chars),
] {
anyhow::ensure!(bound > 0, "harness.{name} must be positive");
}
Ok(())
}
}
pub fn validate_optional(
table: Option<&toml::Table>,
path: &Path,
) -> Result<(), crate::ConfigError> {
let Some(table) = table else {
return Ok(());
};
let harness: Harness =
toml::Value::Table(table.clone())
.try_into()
.map_err(|_| crate::ConfigError::Invalid {
path: path.to_path_buf(),
message: "[harness] table refused".to_owned(),
})?;
harness
.validate()
.map_err(|error| crate::ConfigError::Invalid {
path: path.to_path_buf(),
message: error.to_string(),
})
}
pub(super) struct Seat {
pub path: PathBuf,
pub ticket: Config,
pub harness: Harness,
pub name: String,
pub cwd: PathBuf,
}
#[derive(Deserialize)]
struct Written {
server: String,
channel: String,
lys: crate::lys::LysTable,
limits: Limits,
agent: Option<String>,
harness: Harness,
}
impl Seat {
pub fn load(path: &Path) -> anyhow::Result<Self> {
let path = std::fs::canonicalize(path)
.map_err(|error| anyhow::anyhow!("seat file cannot be opened: {}", error.kind()))?;
let text = zeroize::Zeroizing::new(
std::fs::read_to_string(&path)
.map_err(|error| anyhow::anyhow!("seat file cannot be read: {}", error.kind()))?,
);
let table: toml::Table =
toml::from_str(&text).map_err(|_| anyhow::anyhow!("seat file is not valid TOML"))?;
for field in table.keys() {
anyhow::ensure!(
[
"server", "channel", "lys", "limits", "agent", "listen", "harness"
]
.contains(&field.as_str()),
"seat file has an unknown field"
);
}
let file: Written = toml::from_str(&text).map_err(|_| {
anyhow::anyhow!("seat requires server, channel, [lys], [limits] and [harness]")
})?;
file.harness.validate()?;
super::exit::validate_issuer(&file.lys.issuer)?;
let name = if file.lys.key_file.as_os_str().is_empty() {
std::env::var("LYS_AGENT")
.map_err(|_| anyhow::anyhow!("LYS_AGENT is missing or not Unicode"))?
} else {
file.lys.agent.clone()
};
anyhow::ensure!(
!name.is_empty()
&& name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
&& name.len() <= file.harness.content_bytes,
"LYS_AGENT must name one agent segment within content_bytes"
);
anyhow::ensure!(
file.agent.as_ref().is_none_or(|agent| agent == &name)
&& (file.lys.agent.is_empty() || file.lys.agent == name),
"seat agent differs from LYS_AGENT"
);
validate_listen(table.get("listen"), &name, file.harness.content_bytes)?;
anyhow::ensure!(
!file.server.is_empty() && !file.channel.is_empty(),
"seat server and channel must be nonempty"
);
let limits = file.limits;
anyhow::ensure!(
limits.frame_bytes > 0
&& limits.record_bytes > 0
&& limits.reply_timeout_ms > 0
&& limits.flow_steps > 0
&& limits.request_bytes > 0,
"every ticket limit must be positive"
);
let cwd = file.harness.cwd.clone().map_or_else(
|| {
std::env::current_dir()
.map_err(|_| anyhow::anyhow!("Lys launch directory cannot be read"))
},
Ok,
)?;
Ok(Self {
path,
name: name.clone(),
cwd,
harness: file.harness,
ticket: Config {
server: file.server,
channel: file.channel,
credential: Credential::Lys(file.lys),
limits,
agent: Some(name),
listen: crate::mcp::listen_config::ListenConfig::default(),
},
})
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Listen {
#[serde(default)]
subjects: Vec<String>,
#[serde(rename = "listen_bound")]
bound: Option<usize>,
content_bytes: Option<usize>,
}
pub(super) fn validate_listen(
value: Option<&toml::Value>,
name: &str,
content_bytes: usize,
) -> anyhow::Result<()> {
let Some(value) = value else {
return Ok(());
};
let listen: Listen = value
.clone()
.try_into()
.map_err(|_| anyhow::anyhow!("[listen] table refused"))?;
anyhow::ensure!(
listen.bound.is_none_or(|bound| bound > 0),
"listen_bound must be positive"
);
anyhow::ensure!(
listen
.content_bytes
.is_none_or(|bound| bound == content_bytes),
"[listen] content_bytes must equal [harness] content_bytes"
);
anyhow::ensure!(
listen.subjects.len() <= 1
&& listen
.subjects
.iter()
.all(|subject| subject == &format!("agents.{name}.inbox")),
"agent host accepts only its own inbox in [listen]; additional subjects are not wired"
);
Ok(())
}