use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum ShellEngine {
#[default]
SafeSubset,
Host,
Brush,
}
impl ShellEngine {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::SafeSubset => "safe-subset",
Self::Host => "host",
Self::Brush => "brush",
}
}
pub fn validate_available(self) -> Result<(), String> {
self.validate_worker_transport(agent_bridle_tool_shell::brush_private_control_supported())
}
fn validate_worker_transport(self, supported: bool) -> Result<(), String> {
if self == Self::Brush && !supported {
return Err("brush is unavailable on this platform: authenticated worker transport is not implemented; select safe-subset (portable, limited shell grammar)".into());
}
Ok(())
}
}
impl std::fmt::Display for ShellEngine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for ShellEngine {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().replace('_', "-").as_str() {
"safe-subset" | "subset" | "safe" => Ok(Self::SafeSubset),
"host" | "sandbox-host" | "landlock" | "seatbelt" => Ok(Self::Host),
"brush" | "brush-ocap" => Ok(Self::Brush),
other => Err(format!(
"unknown shell engine '{other}' (expected one of: safe-subset, host, brush)"
)),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct IntakeConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub research: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub explain: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub collaborative: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub direct_verbs: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub question_mark_disposition: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub adjudicate_decisions: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub adjudicator: Option<crate::config::BackendRef>,
}
impl IntakeConfig {
#[must_use]
pub fn adjudicate_decisions_enabled(&self, interactive: bool) -> bool {
self.adjudicate_decisions.unwrap_or(interactive)
}
#[must_use]
pub fn to_lexicon(&self) -> crate::agentic::DispositionLexicon {
let mut lex = crate::agentic::DispositionLexicon::default();
if let Some(list) = &self.action {
lex.action = list.clone();
}
if let Some(list) = &self.research {
lex.research = list.clone();
}
if let Some(list) = &self.explain {
lex.explain = list.clone();
}
if let Some(list) = &self.collaborative {
lex.collaborative = list.clone();
}
if let Some(list) = &self.direct_verbs {
lex.direct_verbs = list.clone();
}
match self.question_mark_disposition.as_deref() {
Some("research") => {
lex.question_mark_disposition = crate::agentic::PromptDisposition::Research;
}
Some("act") => {
lex.question_mark_disposition = crate::agentic::PromptDisposition::Act;
}
_ => {}
}
lex
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct ShellConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub windows_cmd: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub engine: Option<ShellEngine>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub env_passthrough: Option<Vec<String>>,
}
#[must_use]
pub fn shell_env_passthrough_default() -> Vec<String> {
vec!["HOME".to_string(), "USER".to_string(), "TZ".to_string()]
}
#[must_use]
pub fn mcp_stdio_env_passthrough() -> Vec<&'static str> {
vec![
"HOME",
"USER",
"TZ",
"PATH",
"LANG",
"LC_ALL",
"TERM",
"TMPDIR",
"SHELL",
"PYTHONPATH",
"NODE_PATH",
"LD_LIBRARY_PATH",
"DYLD_LIBRARY_PATH",
]
}
#[must_use]
pub fn full_access_default_engine() -> ShellEngine {
full_access_default_with_worker_transport(
agent_bridle_tool_shell::brush_private_control_supported(),
)
}
fn full_access_default_with_worker_transport(supported: bool) -> ShellEngine {
if !supported {
return ShellEngine::SafeSubset;
}
#[cfg(windows)]
{
ShellEngine::Brush
}
#[cfg(not(windows))]
{
ShellEngine::Host
}
}
#[must_use]
pub fn resolve_shell_engine(
cli: Option<ShellEngine>,
configured: Option<ShellEngine>,
full_access: bool,
) -> ShellEngine {
resolve_shell_engine_choice(cli, configured, full_access).unwrap_or(ShellEngine::SafeSubset)
}
#[must_use]
pub fn resolve_shell_engine_choice(
cli: Option<ShellEngine>,
configured: Option<ShellEngine>,
full_access: bool,
) -> Option<ShellEngine> {
if let Some(explicit) = cli.or(configured) {
return Some(explicit);
}
if full_access {
return Some(full_access_default_engine());
}
None
}
#[must_use]
pub fn confined_default_engine(l3_active: bool) -> ShellEngine {
confined_default_with_worker_transport(
l3_active,
agent_bridle_tool_shell::brush_private_control_supported(),
)
}
fn confined_default_with_worker_transport(l3_active: bool, supported: bool) -> ShellEngine {
if l3_active && supported {
ShellEngine::Brush
} else {
ShellEngine::SafeSubset
}
}
#[must_use]
pub fn resolved_confined_default() -> ShellEngine {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
confined_default_engine(ocap_l3_backend().1)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
ShellEngine::SafeSubset
}
}
#[must_use]
pub fn ocap_l3_backend() -> (&'static str, bool) {
#[cfg(target_os = "linux")]
{
("Landlock", agent_bridle::landlock_is_supported())
}
#[cfg(target_os = "macos")]
{
("Seatbelt", agent_bridle::seatbelt_is_supported())
}
#[cfg(target_os = "windows")]
{
("AppContainer", true)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
("none", false)
}
}
#[cfg(test)]
mod shell_engine_tests {
use super::{
confined_default_engine, confined_default_with_worker_transport,
full_access_default_engine, full_access_default_with_worker_transport,
resolve_shell_engine, resolve_shell_engine_choice, shell_env_passthrough_default,
IntakeConfig, ShellConfig, ShellEngine,
};
use crate::config::Config;
#[test]
fn env_passthrough_default_is_minimal() {
assert_eq!(
shell_env_passthrough_default(),
vec!["HOME".to_string(), "USER".to_string(), "TZ".to_string()]
);
}
#[test]
fn unavailable_worker_transport_keeps_full_access_portable() {
assert_eq!(
full_access_default_with_worker_transport(false),
ShellEngine::SafeSubset
);
assert_eq!(
full_access_default_with_worker_transport(true),
if cfg!(windows) {
ShellEngine::Brush
} else {
ShellEngine::Host
}
);
assert_eq!(
confined_default_with_worker_transport(true, false),
ShellEngine::SafeSubset
);
assert_eq!(
confined_default_with_worker_transport(false, true),
ShellEngine::SafeSubset
);
assert_eq!(
confined_default_with_worker_transport(true, true),
ShellEngine::Brush
);
}
#[test]
fn explicit_brush_requires_transport_while_explicit_host_is_preserved() {
let unavailable = ShellEngine::Brush
.validate_worker_transport(false)
.unwrap_err();
assert!(unavailable.contains("authenticated worker transport"));
assert!(unavailable.contains("safe-subset"));
assert!(ShellEngine::Brush.validate_worker_transport(true).is_ok());
assert!(ShellEngine::Host.validate_worker_transport(false).is_ok());
assert!(ShellEngine::SafeSubset
.validate_worker_transport(false)
.is_ok());
}
#[test]
fn from_str_canonical_and_aliases() {
assert_eq!(
"safe-subset".parse::<ShellEngine>().unwrap(),
ShellEngine::SafeSubset
);
assert_eq!(
"subset".parse::<ShellEngine>().unwrap(),
ShellEngine::SafeSubset
);
assert_eq!("host".parse::<ShellEngine>().unwrap(), ShellEngine::Host);
assert_eq!(
"landlock".parse::<ShellEngine>().unwrap(),
ShellEngine::Host
);
assert_eq!("BRUSH".parse::<ShellEngine>().unwrap(), ShellEngine::Brush);
assert_eq!(
"brush-ocap".parse::<ShellEngine>().unwrap(),
ShellEngine::Brush
);
assert!("bogus".parse::<ShellEngine>().is_err());
}
#[test]
fn as_str_roundtrips() {
for e in [
ShellEngine::SafeSubset,
ShellEngine::Host,
ShellEngine::Brush,
] {
assert_eq!(e.as_str().parse::<ShellEngine>().unwrap(), e);
}
}
#[test]
fn resolve_flag_wins_over_everything() {
assert_eq!(
resolve_shell_engine(
Some(ShellEngine::SafeSubset),
Some(ShellEngine::Brush),
true
),
ShellEngine::SafeSubset,
"explicit --shell-engine wins even over config and --full-access"
);
}
#[test]
fn resolve_config_wins_over_full_access_auto_upgrade() {
assert_eq!(
resolve_shell_engine(None, Some(ShellEngine::SafeSubset), true),
ShellEngine::SafeSubset,
"an explicit [shell] engine is not overridden by --full-access"
);
}
#[test]
fn resolve_full_access_auto_upgrades_when_unset() {
assert_eq!(
resolve_shell_engine(None, None, true),
full_access_default_engine()
);
#[cfg(any(target_os = "linux", target_os = "macos"))]
assert_eq!(resolve_shell_engine(None, None, true), ShellEngine::Host);
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
assert_eq!(
resolve_shell_engine(None, None, true),
ShellEngine::SafeSubset
);
}
#[test]
fn resolve_defaults_to_safe_subset() {
assert_eq!(
resolve_shell_engine(None, None, false),
ShellEngine::SafeSubset
);
}
#[test]
fn confined_default_is_l3_gated() {
assert_eq!(
confined_default_engine(true),
if agent_bridle_tool_shell::brush_private_control_supported() {
ShellEngine::Brush
} else {
ShellEngine::SafeSubset
}
);
assert_eq!(confined_default_engine(false), ShellEngine::SafeSubset);
}
#[test]
fn choice_is_none_only_for_the_confined_default() {
assert_eq!(
resolve_shell_engine_choice(Some(ShellEngine::Brush), None, false),
Some(ShellEngine::Brush)
);
assert_eq!(
resolve_shell_engine_choice(None, Some(ShellEngine::Host), false),
Some(ShellEngine::Host)
);
assert_eq!(
resolve_shell_engine_choice(None, None, true),
Some(full_access_default_engine())
);
assert_eq!(resolve_shell_engine_choice(None, None, false), None);
}
#[test]
fn shell_config_deserializes_kebab_case() {
let cfg: ShellConfig = toml::from_str("engine = \"host\"").unwrap();
assert_eq!(cfg.engine, Some(ShellEngine::Host));
let cfg: ShellConfig = toml::from_str("engine = \"safe-subset\"").unwrap();
assert_eq!(cfg.engine, Some(ShellEngine::SafeSubset));
}
#[test]
fn intake_config_defaults_are_all_unset_and_resolve_to_builtin_lexicon() {
let cfg: IntakeConfig = toml::from_str("").unwrap();
assert_eq!(cfg, IntakeConfig::default());
assert_eq!(
cfg.to_lexicon(),
crate::agentic::DispositionLexicon::default(),
"an empty [intake] must resolve to exactly the built-in defaults"
);
}
#[test]
fn intake_config_overrides_round_trip_and_resolve() {
let cfg: IntakeConfig = toml::from_str(
r#"
explain = ["tell me about"]
collaborative = ["let us "]
question_mark_disposition = "research"
"#,
)
.unwrap();
let echoed: IntakeConfig = toml::from_str(&toml::to_string(&cfg).unwrap()).unwrap();
assert_eq!(echoed, cfg);
let lex = cfg.to_lexicon();
assert_eq!(
lex.explain,
vec!["tell me about".to_string()],
"a present list REPLACES its default wholesale"
);
assert_eq!(
lex.collaborative,
vec!["let us ".to_string()],
"the opener list replaces its default wholesale too"
);
assert_eq!(
lex.action,
crate::agentic::DispositionLexicon::default().action,
"an absent list keeps the built-in default"
);
assert_eq!(
lex.question_mark_disposition,
crate::agentic::PromptDisposition::Research
);
let odd: IntakeConfig = toml::from_str("question_mark_disposition = \"bogus\"").unwrap();
assert_eq!(
odd.to_lexicon().question_mark_disposition,
crate::agentic::PromptDisposition::Explain
);
}
#[test]
fn config_root_parses_the_intake_table() {
let cfg: Config = toml::from_str(
r#"
[intake]
action = ["deploy"]
"#,
)
.unwrap();
assert_eq!(
cfg.intake.as_ref().and_then(|i| i.action.clone()),
Some(vec!["deploy".to_string()])
);
}
}
static WINDOWS_CMD: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
pub(crate) fn set_windows_cmd(value: bool) {
WINDOWS_CMD.store(value, std::sync::atomic::Ordering::Relaxed);
}
pub fn windows_cmd_enabled() -> bool {
windows_cmd_choice(
WINDOWS_CMD.load(std::sync::atomic::Ordering::Relaxed),
std::env::var("NEWT_WINDOWS_CMD").ok().as_deref(),
)
}
fn windows_cmd_choice(configured: bool, explicit: Option<&str>) -> bool {
explicit
.map(|value| value == "1" || value.eq_ignore_ascii_case("true"))
.unwrap_or(configured)
}
#[cfg(test)]
mod windows_cmd_tests {
#[test]
fn windows_2776_config_defaults_to_brush_and_accepts_cmd_opt_out() {
let default: super::ShellConfig = toml::from_str("").unwrap();
assert_eq!(default.windows_cmd, None);
let cmd: super::ShellConfig = toml::from_str("windows_cmd = true").unwrap();
assert_eq!(cmd.windows_cmd, Some(true));
assert_eq!(cmd.engine, None, "dialect does not grant authority");
assert!(!super::windows_cmd_choice(false, None));
assert!(super::windows_cmd_choice(true, None));
assert!(
super::windows_cmd_choice(false, Some("1")),
"CLI opt-out wins"
);
assert!(!super::windows_cmd_choice(true, Some("0")));
}
}