diff --git a/crates/goose-server/src/commands/agent.rs b/crates/goose-server/src/commands/agent.rs index ef9be3ac7..5311138a8 100644 --- a/crates/goose-server/src/commands/agent.rs +++ b/crates/goose-server/src/commands/agent.rs @@ -47,8 +47,6 @@ pub async fn run() -> Result<()> { boot_marker("main entered"); crate::logging::setup_logging(Some("goosed"))?; - goose::security::set_security_defaults(); - let settings = configuration::Settings::new()?; let secret_key = std::env::var("GOOSE_SERVER__SECRET_KEY") diff --git a/crates/goose/src/security/mod.rs b/crates/goose/src/security/mod.rs index c6938f2ce..845af16c6 100644 --- a/crates/goose/src/security/mod.rs +++ b/crates/goose/src/security/mod.rs @@ -10,30 +10,16 @@ use crate::conversation::message::{Message, ToolRequest}; use crate::permission::permission_judge::PermissionCheckResult; use anyhow::Result; use scanner::PromptInjectionScanner; +use std::env; use std::sync::OnceLock; use uuid::Uuid; -fn set_default_if_not_exist(config: &Config, key: &str, default_env: &str) { - if config.get_param::(key).is_ok() { - return; - } - if let Ok(parsed) = config.get_param::(default_env) { - let _ = config.set_param(key, parsed); - } -} - -pub fn set_security_defaults() { - let config = Config::global(); - set_default_if_not_exist( - config, - "SECURITY_PROMPT_ENABLED", - "DEFAULT_SECURITY_PROMPT_ENABLED", - ); - set_default_if_not_exist( - config, - "SECURITY_COMMAND_CLASSIFIER_ENABLED", - "DEFAULT_SECURITY_COMMAND_CLASSIFIER_ENABLED", - ); +pub(crate) fn get_override(env_key: &str) -> Option { + env::var(env_key).ok().and_then(|v| match v.as_str() { + "true" => Some(true), + "false" => Some(false), + _ => None, + }) } pub struct SecurityManager { @@ -52,15 +38,17 @@ pub struct SecurityResult { impl SecurityManager { pub fn new() -> Self { - set_security_defaults(); Self { scanner: OnceLock::new(), } } pub fn is_prompt_injection_detection_enabled(&self) -> bool { - let config = Config::global(); + if let Some(overridden) = get_override("SECURITY_PROMPT_ENABLED_OVERRIDE") { + return overridden; + } + let config = Config::global(); config .get_param::("SECURITY_PROMPT_ENABLED") .unwrap_or(false) @@ -73,9 +61,15 @@ impl SecurityManager { .get_param::("SECURITY_PROMPT_CLASSIFIER_ENABLED") .unwrap_or(false); - let command_enabled = config - .get_param::("SECURITY_COMMAND_CLASSIFIER_ENABLED") - .unwrap_or(false); + let command_enabled = if let Some(overridden) = + get_override("SECURITY_COMMAND_CLASSIFIER_ENABLED_OVERRIDE") + { + overridden + } else { + config + .get_param::("SECURITY_COMMAND_CLASSIFIER_ENABLED") + .unwrap_or(false) + }; prompt_enabled || command_enabled } @@ -95,9 +89,14 @@ impl SecurityManager { let scanner = self.scanner.get_or_init(|| { let config = Config::global(); - let command_classifier_enabled = config - .get_param::("SECURITY_COMMAND_CLASSIFIER_ENABLED") - .unwrap_or(false); + let command_classifier_enabled = + if let Some(overridden) = get_override("SECURITY_COMMAND_CLASSIFIER_ENABLED_OVERRIDE") { + overridden + } else { + config + .get_param::("SECURITY_COMMAND_CLASSIFIER_ENABLED") + .unwrap_or(false) + }; let prompt_classifier_enabled = config .get_param::("SECURITY_PROMPT_CLASSIFIER_ENABLED") .unwrap_or(false); diff --git a/crates/goose/src/security/scanner.rs b/crates/goose/src/security/scanner.rs index 8d0e4e4c4..d4d15da52 100644 --- a/crates/goose/src/security/scanner.rs +++ b/crates/goose/src/security/scanner.rs @@ -68,9 +68,19 @@ impl PromptInjectionScanner { ClassifierType::Prompt => "PROMPT", }; - let enabled = config - .get_param::(&format!("SECURITY_{}_CLASSIFIER_ENABLED", prefix)) - .unwrap_or(false); + let enabled = match classifier_type { + ClassifierType::Command => { + crate::security::get_override("SECURITY_COMMAND_CLASSIFIER_ENABLED_OVERRIDE") + .unwrap_or_else(|| { + config + .get_param::("SECURITY_COMMAND_CLASSIFIER_ENABLED") + .unwrap_or(false) + }) + } + ClassifierType::Prompt => config + .get_param::("SECURITY_PROMPT_CLASSIFIER_ENABLED") + .unwrap_or(false), + }; if !enabled { anyhow::bail!("{} classifier not enabled", prefix); diff --git a/ui/desktop/src/components/settings/security/SecurityToggle.tsx b/ui/desktop/src/components/settings/security/SecurityToggle.tsx index d7fb0ffe2..d75958dff 100644 --- a/ui/desktop/src/components/settings/security/SecurityToggle.tsx +++ b/ui/desktop/src/components/settings/security/SecurityToggle.tsx @@ -65,6 +65,15 @@ const i18n = defineMessages({ id: 'securityToggle.apiTokenDescription', defaultMessage: 'Authentication token for the classification service', }, + overrideNotice: { + id: 'securityToggle.overrideNotice', + defaultMessage: 'This setting is managed by your organization and cannot be changed.', + }, + warpNotice: { + id: 'securityToggle.warpNotice', + defaultMessage: + 'Command injection detection works best when connected to WARP (required to reach the classification service).', + }, commandEndpointDescription: { id: 'securityToggle.commandEndpointDescription', defaultMessage: 'Enter the full URL for your command injection classification service', @@ -175,6 +184,19 @@ export const SecurityToggle = () => { const intl = useIntl(); const { config, upsert } = useConfig(); + const promptEnabledOverride = window.appConfig?.get('SECURITY_PROMPT_ENABLED_OVERRIDE') as + | string + | undefined; + const commandClassifierOverride = window.appConfig?.get( + 'SECURITY_COMMAND_CLASSIFIER_ENABLED_OVERRIDE' + ) as string | undefined; + const isPromptOverridden = + promptEnabledOverride === 'true' || promptEnabledOverride === 'false'; + const isCommandClassifierOverridden = + commandClassifierOverride === 'true' || commandClassifierOverride === 'false'; + const promptOverrideValue = promptEnabledOverride === 'true'; + const commandClassifierOverrideValue = commandClassifierOverride === 'true'; + const modelMapping = useMemo(() => { const mappingEnv = window.appConfig?.get('SECURITY_ML_MODEL_MAPPING') as string | undefined; if (!mappingEnv) { @@ -224,7 +246,9 @@ export const SecurityToggle = () => { return Object.values(modelMapping).some((modelInfo) => modelInfo.model_type === 'command'); }, [modelMapping]); - const effectiveCommandClassifierEnabled = commandClassifierEnabled ?? false; + const effectiveCommandClassifierEnabled = isCommandClassifierOverridden + ? commandClassifierOverrideValue + : (commandClassifierEnabled ?? false); const effectiveModel = mlModel || availablePromptModels[0]?.value || ''; const [thresholdInput, setThresholdInput] = useState(configThreshold.toString()); const [endpointInput, setEndpointInput] = useState(mlEndpoint); @@ -290,6 +314,8 @@ export const SecurityToggle = () => { await upsert('SECURITY_COMMAND_CLASSIFIER_TOKEN', token, true); // true = secret }; + const effectiveEnabled = isPromptOverridden ? promptOverrideValue : enabled; + return (
@@ -298,22 +324,32 @@ export const SecurityToggle = () => {

{intl.formatMessage(i18n.promptInjectionDescription)}

+ {isPromptOverridden && ( +

+ {intl.formatMessage(i18n.overrideNotice)} +

+ )}
-
- +
+
{/* Detection Threshold */} -
+
@@ -338,9 +374,9 @@ export const SecurityToggle = () => { handleThresholdChange(value); } }} - disabled={!enabled} + disabled={!effectiveEnabled} className={`w-24 px-2 py-1 text-sm border rounded ${ - enabled + effectiveEnabled ? 'border-border-primary bg-background-primary text-text-primary' : 'border-border-primary bg-background-secondary text-text-secondary cursor-not-allowed' }`} @@ -353,26 +389,40 @@ export const SecurityToggle = () => {

{intl.formatMessage(i18n.enableCommandInjection)}

{intl.formatMessage(i18n.commandInjectionDescription)}

+ {isCommandClassifierOverridden && ( + <> +

+ {intl.formatMessage(i18n.overrideNotice)} +

+ {commandClassifierOverrideValue && ( +

+ {intl.formatMessage(i18n.warpNotice)} +

+ )} + + )}
-
+
{hasCommandModel ? ( - enabled && + effectiveEnabled && effectiveCommandClassifierEnabled && (
✓ {intl.formatMessage(i18n.commandClassifierActive)} @@ -381,12 +431,16 @@ export const SecurityToggle = () => { ) : (
-
+
{ onTokenChange={setCommandTokenInput} onEndpointBlur={handleCommandEndpointChange} onTokenBlur={handleCommandTokenChange} - disabled={!enabled || !effectiveCommandClassifierEnabled} + disabled={!effectiveEnabled || !effectiveCommandClassifierEnabled} endpointPlaceholder="https://example.com/classify" tokenPlaceholder="token..." endpointLabel={intl.formatMessage(i18n.classificationEndpoint)} @@ -412,7 +466,7 @@ export const SecurityToggle = () => {

{intl.formatMessage(i18n.enablePromptInjectionMl)}

@@ -424,7 +478,7 @@ export const SecurityToggle = () => {
@@ -433,15 +487,17 @@ export const SecurityToggle = () => { {/* Configuration Section */}
-
+
{showModelDropdown ? (
@@ -451,9 +507,9 @@ export const SecurityToggle = () => {