use serde::Deserialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CompletionGateMode {
#[default]
Observe,
Enforce,
}
impl CompletionGateMode {
#[must_use]
pub fn from_str_lossy(s: &str) -> Self {
match s.trim().to_ascii_lowercase().as_str() {
"enforce" => Self::Enforce,
_ => Self::Observe,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GenericGateMode {
#[default]
Off,
Observe,
Enforce,
}
impl GenericGateMode {
#[must_use]
pub fn from_optional_str(s: Option<&str>) -> Self {
match s.map(str::trim).map(str::to_ascii_lowercase).as_deref() {
Some("enforce") => Self::Enforce,
Some("observe") => Self::Observe,
_ => Self::Off,
}
}
#[must_use]
pub fn is_on(self) -> bool {
!matches!(self, Self::Off)
}
#[must_use]
pub fn is_enforce(self) -> bool {
matches!(self, Self::Enforce)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum VerifySource {
#[default]
Operator,
ModelDeclared,
Toolchain,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ManifestShell {
#[default]
Default,
Pwsh,
Bash,
Cmd,
None,
}
impl ManifestShell {
#[must_use]
pub fn from_optional_str(s: Option<&str>) -> Self {
match s.map(str::trim).map(str::to_ascii_lowercase) {
Some(ref v) if v == "pwsh" || v == "powershell" => Self::Pwsh,
Some(ref v) if v == "bash" => Self::Bash,
Some(ref v) if v == "cmd" => Self::Cmd,
Some(ref v) if v == "none" => Self::None,
_ => Self::Default,
}
}
}
#[derive(Debug, Clone)]
pub struct CompletionGateVerifyEntry {
pub id: String,
pub cmd: Option<String>,
pub argv: Vec<String>,
pub shell: ManifestShell,
pub timeout_secs: u32,
pub source: VerifySource,
}
#[derive(Debug, Clone)]
pub struct CompletionGateDeliverableEntry {
pub id: String,
pub path: Option<String>,
pub glob: Option<String>,
pub optional_verify_cmd: Option<String>,
pub tracked: bool,
}
#[derive(Debug, Clone, Default)]
pub struct CompletionGateConfig {
pub mode: CompletionGateMode,
pub max_manifest_rounds: u32,
pub max_audit_rounds: u32,
pub max_infra_strikes: u32,
pub verify: Vec<CompletionGateVerifyEntry>,
pub deliverable: Vec<CompletionGateDeliverableEntry>,
pub auto_verify_replay: GenericGateMode,
pub toolchain_gate: GenericGateMode,
pub stub_gate: GenericGateMode,
pub min_lines: MinLinesGateConfig,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MinLinesGateConfig {
pub frontend: Option<u32>,
pub backend: Option<u32>,
pub frontend_glob: Option<String>,
pub backend_glob: Option<String>,
}
impl MinLinesGateConfig {
#[must_use]
pub fn is_active(&self) -> bool {
self.frontend.is_some() || self.backend.is_some()
}
}
impl CompletionGateConfig {
#[must_use]
pub fn is_active(&self) -> bool {
!self.verify.is_empty()
|| !self.deliverable.is_empty()
|| self.auto_verify_replay.is_on()
|| self.toolchain_gate.is_on()
|| self.stub_gate.is_on()
|| self.min_lines.is_active()
}
#[must_use]
pub fn has_layer2(&self) -> bool {
!self.verify.is_empty() || self.auto_verify_replay.is_on() || self.toolchain_gate.is_on()
}
#[must_use]
pub fn has_layer3(&self) -> bool {
!self.deliverable.is_empty() || self.min_lines.is_active()
}
#[must_use]
pub fn sanitized_for_source(mut self, trusted: bool) -> Self {
if !trusted && self.mode == CompletionGateMode::Enforce {
self.mode = CompletionGateMode::Observe;
}
if !trusted && self.stub_gate.is_enforce() {
self.stub_gate = GenericGateMode::Observe;
}
self
}
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct CompletionGateConfigToml {
#[serde(default)]
pub mode: Option<String>,
#[serde(default)]
pub max_manifest_rounds: Option<u32>,
#[serde(default)]
pub max_audit_rounds: Option<u32>,
#[serde(default)]
pub max_infra_strikes: Option<u32>,
#[serde(default)]
pub verify: Vec<CompletionGateVerifyToml>,
#[serde(default)]
pub deliverable: Vec<CompletionGateDeliverableToml>,
#[serde(default)]
pub auto_verify_replay: Option<String>,
#[serde(default)]
pub toolchain_gate: Option<String>,
#[serde(default)]
pub stub_gate: Option<String>,
#[serde(default)]
pub min_lines: Option<MinLinesGateToml>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct MinLinesGateToml {
#[serde(default)]
pub frontend: Option<u32>,
#[serde(default)]
pub backend: Option<u32>,
#[serde(default)]
pub frontend_glob: Option<String>,
#[serde(default)]
pub backend_glob: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct CompletionGateVerifyToml {
pub id: String,
#[serde(default)]
pub cmd: Option<String>,
#[serde(default)]
pub argv: Option<Vec<String>>,
#[serde(default)]
pub shell: Option<String>,
#[serde(default)]
pub timeout_secs: Option<u32>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct CompletionGateDeliverableToml {
pub id: String,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub glob: Option<String>,
#[serde(default)]
pub optional_verify_cmd: Option<String>,
#[serde(default)]
pub tracked: Option<bool>,
}
impl CompletionGateConfigToml {
#[must_use]
pub fn into_runtime(self) -> CompletionGateConfig {
CompletionGateConfig {
mode: self
.mode
.map(|m| CompletionGateMode::from_str_lossy(&m))
.unwrap_or_default(),
max_manifest_rounds: self.max_manifest_rounds.unwrap_or(5),
max_audit_rounds: self.max_audit_rounds.unwrap_or(5),
max_infra_strikes: self.max_infra_strikes.unwrap_or(3),
verify: self.verify.into_iter().map(Into::into).collect(),
deliverable: self.deliverable.into_iter().map(Into::into).collect(),
auto_verify_replay: GenericGateMode::from_optional_str(
self.auto_verify_replay.as_deref(),
),
toolchain_gate: GenericGateMode::from_optional_str(self.toolchain_gate.as_deref()),
stub_gate: self
.stub_gate
.as_deref()
.map(|s| GenericGateMode::from_optional_str(Some(s)))
.unwrap_or(GenericGateMode::Observe),
min_lines: self
.min_lines
.map(|m| MinLinesGateConfig {
frontend: m.frontend,
backend: m.backend,
frontend_glob: m.frontend_glob,
backend_glob: m.backend_glob,
})
.unwrap_or_default(),
}
}
}
impl From<CompletionGateVerifyToml> for CompletionGateVerifyEntry {
fn from(v: CompletionGateVerifyToml) -> Self {
Self {
id: v.id,
cmd: v.cmd,
argv: v.argv.unwrap_or_default(),
shell: ManifestShell::from_optional_str(v.shell.as_deref()),
timeout_secs: v.timeout_secs.unwrap_or(300),
source: VerifySource::Operator,
}
}
}
impl From<CompletionGateDeliverableToml> for CompletionGateDeliverableEntry {
fn from(d: CompletionGateDeliverableToml) -> Self {
Self {
id: d.id,
path: d.path,
glob: d.glob,
optional_verify_cmd: d.optional_verify_cmd,
tracked: d.tracked.unwrap_or(false),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn untrusted_source_downgrades_enforce_to_observe() {
let cfg = CompletionGateConfig {
mode: CompletionGateMode::Enforce,
..Default::default()
};
assert_eq!(
cfg.clone().sanitized_for_source(true).mode,
CompletionGateMode::Enforce
);
assert_eq!(
cfg.sanitized_for_source(false).mode,
CompletionGateMode::Observe
);
}
#[test]
fn untrusted_source_leaves_observe_unchanged() {
let cfg = CompletionGateConfig {
mode: CompletionGateMode::Observe,
..Default::default()
};
assert_eq!(
cfg.sanitized_for_source(false).mode,
CompletionGateMode::Observe
);
}
}