#[cfg(feature = "alloc")]
use alloc::string::String;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
#[cfg(feature = "std")]
use std::path::PathBuf;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum GovernanceLevel {
#[default]
L0Discover,
L1Observe,
L2Enforce,
L3Native,
}
impl core::fmt::Display for GovernanceLevel {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
GovernanceLevel::L0Discover => write!(f, "L0Discover"),
GovernanceLevel::L1Observe => write!(f, "L1Observe"),
GovernanceLevel::L2Enforce => write!(f, "L2Enforce"),
GovernanceLevel::L3Native => write!(f, "L3Native"),
}
}
}
impl core::str::FromStr for GovernanceLevel {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"L0" | "L0Discover" => Ok(GovernanceLevel::L0Discover),
"L1" | "L1Observe" => Ok(GovernanceLevel::L1Observe),
"L2" | "L2Enforce" => Ok(GovernanceLevel::L2Enforce),
"L3" | "L3Native" => Ok(GovernanceLevel::L3Native),
_ => Err("unknown governance level; expected L0, L1, L2, or L3"),
}
}
}
#[cfg(feature = "alloc")]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum DevToolKind {
ClaudeCode,
Codex,
GitHubCopilot,
WindsurfCascade,
Custom(String),
}
#[cfg(feature = "alloc")]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct McpServerInfo {
pub name: String,
pub command: String,
pub args: Vec<String>,
}
#[cfg(feature = "std")]
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum AdapterError {
#[error("dev tool not found on this host")]
ToolNotFound,
#[error("dev tool detection failed: {0}")]
DetectionFailed(String),
#[error("managed-settings generation failed: {0}")]
SettingsGenerationFailed(String),
#[error("managed-settings apply failed: {0}")]
SettingsApplyFailed(std::io::Error),
#[error("launch command construction failed: {0}")]
LaunchFailed(String),
#[error("MCP configuration failed: {0}")]
McpConfigFailed(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("serialization error: {0}")]
Serde(String),
}
#[cfg(feature = "std")]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DevToolInfo {
pub kind: DevToolKind,
pub version: Option<String>,
pub install_path: PathBuf,
pub governance_level: GovernanceLevel,
pub supports_mcp: bool,
pub supports_managed_settings: bool,
}
#[cfg(feature = "std")]
#[async_trait::async_trait]
pub trait DevToolAdapter: Send + Sync {
fn detect(&self) -> Option<DevToolInfo>;
async fn generate_managed_settings(&self, policy: &crate::policy::PolicyDocument) -> Result<String, AdapterError>;
async fn apply_settings(&self, settings: &str) -> Result<(), AdapterError>;
fn build_launch_command(
&self,
tool_args: &[String],
agent_id: &str,
team_id: Option<&str>,
proxy_addr: Option<&str>,
) -> Result<std::process::Command, AdapterError>;
async fn list_mcp_servers(&self) -> Result<Vec<McpServerInfo>, AdapterError>;
async fn apply_mcp_governance(&self, allowed: &[String], denied: &[String]) -> Result<(), AdapterError>;
fn governance_level(&self) -> GovernanceLevel;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn governance_level_orders_l0_through_l3() {
assert!(GovernanceLevel::L0Discover < GovernanceLevel::L1Observe);
assert!(GovernanceLevel::L1Observe < GovernanceLevel::L2Enforce);
assert!(GovernanceLevel::L2Enforce < GovernanceLevel::L3Native);
assert!(GovernanceLevel::L0Discover < GovernanceLevel::L3Native);
}
#[cfg(all(feature = "serde", feature = "alloc"))]
#[test]
fn dev_tool_kind_round_trips_via_serde_json() {
let cases = [
DevToolKind::ClaudeCode,
DevToolKind::Codex,
DevToolKind::GitHubCopilot,
DevToolKind::WindsurfCascade,
DevToolKind::Custom(String::from("MyEditor")),
];
for original in cases {
let json = serde_json::to_string(&original).expect("serialize");
let restored: DevToolKind = serde_json::from_str(&json).expect("deserialize");
assert_eq!(restored, original);
}
}
#[cfg(all(feature = "serde", feature = "std"))]
#[test]
fn dev_tool_info_round_trips_via_serde_json() {
let original = DevToolInfo {
kind: DevToolKind::ClaudeCode,
version: Some(String::from("1.2.3")),
install_path: PathBuf::from("/usr/local/bin/claude"),
governance_level: GovernanceLevel::L2Enforce,
supports_mcp: true,
supports_managed_settings: false,
};
let json1 = serde_json::to_string(&original).expect("serialize");
let restored: DevToolInfo = serde_json::from_str(&json1).expect("deserialize");
let json2 = serde_json::to_string(&restored).expect("re-serialize");
assert_eq!(json1, json2);
}
#[cfg(feature = "std")]
fn _assert_object_safe(_: &dyn DevToolAdapter) {}
#[cfg(feature = "std")]
fn _assert_send_sync<T: Send + Sync>() {}
#[cfg(feature = "std")]
#[test]
fn trait_is_object_safe() {
let _: fn(&dyn DevToolAdapter) = _assert_object_safe;
_assert_send_sync::<Box<dyn DevToolAdapter>>();
}
}