1use std::collections::BTreeMap;
12
13use serde::{Deserialize, Serialize};
14
15use crate::deployment::DeploymentGrade;
16
17#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
27pub struct FeatureFlags {
28 pub global_chat: bool,
34 pub notifications: bool,
36 pub evals: bool,
38 pub app_budgets: bool,
41 pub agent_versions: bool,
44 pub voice: bool,
46 pub agent_delegation: bool,
50 pub observers: bool,
52 pub public_chat: bool,
56 pub mcp_endpoint: bool,
60}
61
62#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
69#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
70#[serde(transparent)]
71pub struct FeatureFlagMap(pub BTreeMap<String, bool>);
72
73impl From<&FeatureFlags> for FeatureFlagMap {
74 fn from(flags: &FeatureFlags) -> Self {
75 flags.to_map()
76 }
77}
78
79impl From<FeatureFlags> for FeatureFlagMap {
80 fn from(flags: FeatureFlags) -> Self {
81 flags.to_map()
82 }
83}
84
85#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
87pub struct FeatureFlagDefinition {
88 pub name: &'static str,
90 pub label: &'static str,
92 pub description: &'static str,
94 pub experimental: bool,
96}
97
98pub const API_FEATURE_FLAG_DEFINITIONS: &[FeatureFlagDefinition] = &[
100 FeatureFlagDefinition {
101 name: "global_chat",
102 label: "Platform Chat",
103 description: "Adds a personal Platform Chat assistant you can open from the sidebar or the \
104 /chat page anywhere in the app. It's a quick scratchpad to talk to your agents \
105 without first setting up a dedicated app or channel. Turning it off disables the \
106 whole feature — the chat page, the sidebar entry, and its session and voice APIs all \
107 become unavailable.",
108 experimental: true,
109 },
110 FeatureFlagDefinition {
111 name: "notifications",
112 label: "Notifications",
113 description: "Turns on the in-app notification bell, toasts, and live updates. You get \
114 alerted in real time when something you care about happens, instead of refreshing \
115 or checking back manually.",
116 experimental: true,
117 },
118 FeatureFlagDefinition {
119 name: "evals",
120 label: "Evals",
121 description: "Lets you define and run behavioral evals against your agents. Use it to \
122 confirm an agent responds the way you expect and to catch regressions as you change \
123 prompts or models.",
124 experimental: true,
125 },
126 FeatureFlagDefinition {
127 name: "app_budgets",
128 label: "App budgets",
129 description: "Adds spending limits scoped to individual apps and channels, with automatic \
130 resets on a schedule. It helps you cap and control costs so a single app or channel \
131 can't run away with your usage.",
132 experimental: true,
133 },
134 FeatureFlagDefinition {
135 name: "agent_versions",
136 label: "Agent versions",
137 description: "Captures immutable snapshots of your agents so you can fork, roll back, and \
138 pin apps to a specific version. This gives you a safety net to experiment freely \
139 and return to a known-good agent at any time.",
140 experimental: true,
141 },
142 FeatureFlagDefinition {
143 name: "voice",
144 label: "Voice",
145 description: "Enables realtime voice in chat with microphone controls. You can talk to \
146 your agents and hear responses instead of typing, for a hands-free conversation.",
147 experimental: true,
148 },
149 FeatureFlagDefinition {
150 name: "observers",
151 label: "Observers",
152 description: "Runs automatic online scoring on your production sessions. It continuously \
153 evaluates live conversations so you can monitor quality on real traffic without \
154 manually reviewing each one.",
155 experimental: true,
156 },
157 FeatureFlagDefinition {
158 name: "public_chat",
159 label: "Public Chat",
160 description: "Isolated, public-facing chat web app and the public_chat channel.",
161 experimental: true,
162 },
163 FeatureFlagDefinition {
164 name: "mcp_endpoint",
165 label: "MCP endpoint",
166 description: "Exposes the first-party MCP server at POST /mcp for authenticated MCP \
167 clients. This remote-control surface can list and invoke MCP tools, including \
168 side-effecting operations within the caller's permissions.",
169 experimental: true,
170 },
171];
172
173impl FeatureFlags {
174 pub fn for_org(system: &Self, org_enabled: &std::collections::HashMap<String, bool>) -> Self {
179 let opt_in = |name: &str, system_on: bool| -> bool {
180 system_on && org_enabled.get(name).copied().unwrap_or(false)
181 };
182 Self {
183 global_chat: opt_in("global_chat", system.global_chat),
184 notifications: opt_in("notifications", system.notifications),
185 evals: opt_in("evals", system.evals),
186 app_budgets: opt_in("app_budgets", system.app_budgets),
187 agent_versions: opt_in("agent_versions", system.agent_versions),
188 voice: opt_in("voice", system.voice),
189 agent_delegation: opt_in("agent_delegation", system.agent_delegation),
190 observers: opt_in("observers", system.observers),
191 public_chat: opt_in("public_chat", system.public_chat),
192 mcp_endpoint: opt_in("mcp_endpoint", system.mcp_endpoint),
193 }
194 }
195
196 pub fn from_env(grade: &DeploymentGrade) -> Self {
198 Self {
199 global_chat: experimental_flag("FEATURE_GLOBAL_CHAT", grade),
200 notifications: experimental_flag("FEATURE_NOTIFICATIONS", grade),
201 evals: experimental_flag("FEATURE_EVALS", grade),
202 app_budgets: experimental_flag("FEATURE_APP_BUDGETS", grade),
203 agent_versions: experimental_flag("FEATURE_AGENT_VERSIONS", grade),
204 voice: experimental_flag("FEATURE_VOICE", grade),
205 agent_delegation: experimental_flag("FEATURE_AGENT_DELEGATION", grade),
206 observers: experimental_flag("FEATURE_OBSERVERS", grade),
207 public_chat: experimental_flag("FEATURE_PUBLIC_CHAT", grade),
208 mcp_endpoint: experimental_flag("FEATURE_MCP_ENDPOINT", grade),
209 }
210 }
211
212 pub fn current() -> Self {
215 Self::from_env(&DeploymentGrade::from_env())
216 }
217
218 pub fn to_map(&self) -> FeatureFlagMap {
225 FeatureFlagMap(BTreeMap::from([
226 ("global_chat".to_string(), self.global_chat),
227 ("notifications".to_string(), self.notifications),
228 ("evals".to_string(), self.evals),
229 ("app_budgets".to_string(), self.app_budgets),
230 ("agent_versions".to_string(), self.agent_versions),
231 ("voice".to_string(), self.voice),
232 ("agent_delegation".to_string(), self.agent_delegation),
233 ("observers".to_string(), self.observers),
234 ("public_chat".to_string(), self.public_chat),
235 ("mcp_endpoint".to_string(), self.mcp_endpoint),
236 ]))
237 }
238
239 pub fn is_enabled(&self, flag: &str) -> bool {
241 match flag {
242 "global_chat" => self.global_chat,
243 "notifications" => self.notifications,
244 "evals" => self.evals,
245 "app_budgets" => self.app_budgets,
246 "agent_versions" => self.agent_versions,
247 "voice" => self.voice,
248 "agent_delegation" => self.agent_delegation,
249 "observers" => self.observers,
250 "public_chat" => self.public_chat,
251 "mcp_endpoint" => self.mcp_endpoint,
252 _ => false,
253 }
254 }
255
256 #[cfg(test)]
258 pub fn all_enabled() -> Self {
259 Self {
260 global_chat: true,
261 notifications: true,
262 evals: true,
263 app_budgets: true,
264 agent_versions: true,
265 voice: true,
266 agent_delegation: true,
267 observers: true,
268 public_chat: true,
269 mcp_endpoint: true,
270 }
271 }
272}
273
274#[derive(Debug, Default, Clone, PartialEq, Eq)]
278pub struct InternalFeatureFlags {
279 pub docker_capability: bool,
282 pub container_sandbox: bool,
288 pub session_sandbox: bool,
291 pub machine_payments: bool,
296 pub lua: bool,
300}
301
302impl InternalFeatureFlags {
303 pub fn from_env() -> Self {
305 let docker_capability = standard_flag("FEATURE_DOCKER_CAPABILITY", false);
306
307 Self {
308 docker_capability,
309 container_sandbox: standard_flag("FEATURE_CONTAINER_SANDBOX", docker_capability),
310 session_sandbox: standard_flag("FEATURE_SESSION_SANDBOX", false),
311 machine_payments: standard_flag("FEATURE_MACHINE_PAYMENTS", false),
312 lua: standard_flag("FEATURE_LUA", false),
313 }
314 }
315
316 pub fn is_enabled(&self, flag: &str) -> bool {
318 match flag {
319 "docker_capability" => self.docker_capability,
320 "container_sandbox" => self.container_sandbox,
321 "session_sandbox" => self.session_sandbox,
322 "machine_payments" => self.machine_payments,
323 "lua" => self.lua,
324 _ => false,
325 }
326 }
327}
328
329fn experimental_flag(env_var: &str, grade: &DeploymentGrade) -> bool {
333 if let Ok(val) = std::env::var(env_var) {
334 return val == "true" || val == "1";
335 }
336 grade.experimental_features_enabled()
337}
338
339fn standard_flag(env_var: &str, default: bool) -> bool {
343 std::env::var(env_var)
344 .map(|v| v == "true" || v == "1")
345 .unwrap_or(default)
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
354
355 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
356 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
357 }
358
359 fn restore_env(key: &str, prev: Option<String>) {
361 match prev {
362 Some(value) => unsafe { std::env::set_var(key, value) },
363 None => unsafe { std::env::remove_var(key) },
364 }
365 }
366
367 #[test]
368 fn test_default_flags() {
369 let flags = FeatureFlags::default();
370 assert!(!flags.global_chat);
371 assert!(!flags.notifications);
372 }
373
374 #[test]
378 fn test_experimental_enabled_in_dev() {
379 let _lock = lock_env();
380 unsafe { std::env::remove_var("FEATURE_GLOBAL_CHAT") };
381 unsafe { std::env::remove_var("FEATURE_EVALS") };
382 let flags = FeatureFlags::from_env(&DeploymentGrade::Dev);
383 assert!(flags.global_chat);
384 assert!(flags.evals);
385 }
386
387 #[test]
388 fn test_experimental_disabled_in_prod() {
389 let _lock = lock_env();
390 unsafe { std::env::remove_var("FEATURE_GLOBAL_CHAT") };
391 unsafe { std::env::remove_var("FEATURE_EVALS") };
392 let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
393 assert!(!flags.global_chat);
394 assert!(!flags.evals);
395 }
396
397 #[test]
398 fn test_env_override_enables_in_prod() {
399 let _lock = lock_env();
400 unsafe { std::env::set_var("FEATURE_GLOBAL_CHAT", "true") };
401 let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
402 assert!(flags.global_chat);
403 unsafe { std::env::remove_var("FEATURE_GLOBAL_CHAT") };
404 }
405
406 #[test]
407 fn test_env_override_disables_in_dev() {
408 let _lock = lock_env();
409 unsafe { std::env::set_var("FEATURE_GLOBAL_CHAT", "false") };
410 let flags = FeatureFlags::from_env(&DeploymentGrade::Dev);
411 assert!(!flags.global_chat);
412 unsafe { std::env::remove_var("FEATURE_GLOBAL_CHAT") };
413 }
414
415 #[test]
416 fn test_is_enabled_dynamic() {
417 let flags = FeatureFlags {
418 global_chat: true,
419 notifications: true,
420 evals: true,
421 app_budgets: true,
422 agent_versions: true,
423 voice: true,
424 agent_delegation: true,
425 observers: true,
426 public_chat: true,
427 mcp_endpoint: true,
428 };
429 assert!(flags.is_enabled("global_chat"));
430 assert!(flags.is_enabled("notifications"));
431 assert!(flags.is_enabled("evals"));
432 assert!(flags.is_enabled("app_budgets"));
433 assert!(flags.is_enabled("agent_versions"));
434 assert!(flags.is_enabled("voice"));
435 assert!(flags.is_enabled("agent_delegation"));
436 assert!(flags.is_enabled("observers"));
437 assert!(flags.is_enabled("public_chat"));
438 assert!(flags.is_enabled("mcp_endpoint"));
439 assert!(!flags.is_enabled("nonexistent"));
440 }
441
442 #[test]
443 fn test_serialization() {
444 let flags = FeatureFlags {
445 global_chat: true,
446 notifications: true,
447 evals: true,
448 app_budgets: true,
449 agent_versions: true,
450 voice: true,
451 agent_delegation: true,
452 observers: true,
453 public_chat: true,
454 mcp_endpoint: true,
455 };
456 let json = serde_json::to_string(&flags).unwrap();
457 assert!(json.contains("\"global_chat\":true"));
458 assert!(json.contains("\"notifications\":true"));
459 assert!(json.contains("\"app_budgets\":true"));
460 assert!(json.contains("\"agent_versions\":true"));
461 assert!(json.contains("\"voice\":true"));
462 assert!(json.contains("\"agent_delegation\":true"));
463 assert!(json.contains("\"observers\":true"));
464
465 let parsed: FeatureFlags = serde_json::from_str(&json).unwrap();
466 assert_eq!(flags, parsed);
467 }
468
469 #[test]
470 fn test_to_map_matches_serialized_flags() {
471 let flags = FeatureFlags::all_enabled();
476 let typed: serde_json::Value = serde_json::to_value(&flags).unwrap();
477 let map: serde_json::Value = serde_json::to_value(flags.to_map()).unwrap();
478 assert_eq!(typed, map);
479
480 let default_typed: serde_json::Value =
481 serde_json::to_value(FeatureFlags::default()).unwrap();
482 let default_map: serde_json::Value =
483 serde_json::to_value(FeatureFlags::default().to_map()).unwrap();
484 assert_eq!(default_typed, default_map);
485 }
486
487 #[test]
488 fn test_agent_delegation_enabled_in_dev() {
489 let _lock = lock_env();
490 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
491 let flags = FeatureFlags::from_env(&DeploymentGrade::Dev);
492 assert!(flags.agent_delegation);
493 }
494
495 #[test]
496 fn test_agent_delegation_disabled_in_prod() {
497 let _lock = lock_env();
498 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
499 let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
500 assert!(!flags.agent_delegation);
501 }
502
503 #[test]
504 fn test_agent_delegation_env_override_in_prod() {
505 let _lock = lock_env();
506 unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
507 let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
508 assert!(flags.agent_delegation);
509 unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
510 }
511
512 #[test]
513 fn test_standard_flag() {
514 let _lock = lock_env();
515 unsafe { std::env::remove_var("FEATURE_TEST_STD") };
516 assert!(!standard_flag("FEATURE_TEST_STD", false));
517 assert!(standard_flag("FEATURE_TEST_STD", true));
518
519 unsafe { std::env::set_var("FEATURE_TEST_STD", "1") };
520 assert!(standard_flag("FEATURE_TEST_STD", false));
521 unsafe { std::env::remove_var("FEATURE_TEST_STD") };
522 }
523
524 #[test]
525 fn test_notifications_enabled_in_dev() {
526 let _lock = lock_env();
527 unsafe { std::env::remove_var("FEATURE_NOTIFICATIONS") };
528 let flags = FeatureFlags::from_env(&DeploymentGrade::Dev);
529 assert!(flags.notifications);
530 }
531
532 #[test]
533 fn test_notifications_disabled_in_prod() {
534 let _lock = lock_env();
535 unsafe { std::env::remove_var("FEATURE_NOTIFICATIONS") };
536 let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
537 assert!(!flags.notifications);
538 }
539
540 #[test]
541 fn test_for_org_requires_system_and_opt_in() {
542 let system = FeatureFlags {
543 global_chat: true,
544 evals: true,
545 ..FeatureFlags::default()
546 };
547 let mut org = std::collections::HashMap::new();
548 org.insert("global_chat".to_string(), true);
549 let effective = FeatureFlags::for_org(&system, &org);
550 assert!(effective.global_chat);
551 assert!(!effective.evals);
552
553 let effective_none = FeatureFlags::for_org(&system, &std::collections::HashMap::new());
554 assert!(!effective_none.global_chat);
555 }
556
557 #[test]
558 fn test_for_org_cannot_enable_when_system_off() {
559 let system = FeatureFlags::default();
560 let mut org = std::collections::HashMap::new();
561 org.insert("global_chat".to_string(), true);
562 let effective = FeatureFlags::for_org(&system, &org);
563 assert!(!effective.global_chat);
564 }
565
566 #[test]
567 fn test_notifications_respects_env_override() {
568 let _lock = lock_env();
569 unsafe { std::env::set_var("FEATURE_NOTIFICATIONS", "true") };
570 let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
571 assert!(flags.notifications);
572 unsafe { std::env::remove_var("FEATURE_NOTIFICATIONS") };
573 }
574
575 #[test]
580 fn test_internal_default_flags() {
581 let flags = InternalFeatureFlags::default();
582 assert!(!flags.docker_capability);
583 assert!(!flags.container_sandbox);
584 assert!(!flags.session_sandbox);
585 assert!(!flags.machine_payments);
586 }
587
588 #[test]
589 fn test_docker_capability_flag_disabled_by_default_in_dev() {
590 let _lock = lock_env();
591 unsafe { std::env::remove_var("FEATURE_DOCKER_CAPABILITY") };
592 let flags = InternalFeatureFlags::from_env();
593 assert!(
594 !flags.docker_capability,
595 "docker_capability should be disabled by default even in dev"
596 );
597 }
598
599 #[test]
600 fn test_docker_capability_flag_enabled_by_env_override() {
601 let _lock = lock_env();
602 unsafe { std::env::set_var("FEATURE_DOCKER_CAPABILITY", "true") };
603 let flags = InternalFeatureFlags::from_env();
604 assert!(flags.docker_capability);
605 unsafe { std::env::remove_var("FEATURE_DOCKER_CAPABILITY") };
606 }
607
608 #[test]
609 fn test_container_sandbox_flag_enabled_by_env_override() {
610 let _lock = lock_env();
611 unsafe { std::env::set_var("FEATURE_CONTAINER_SANDBOX", "true") };
612 unsafe { std::env::remove_var("FEATURE_DOCKER_CAPABILITY") };
613 let flags = InternalFeatureFlags::from_env();
614 assert!(flags.container_sandbox);
615 unsafe { std::env::remove_var("FEATURE_CONTAINER_SANDBOX") };
616 }
617
618 #[test]
619 fn test_container_sandbox_flag_falls_back_to_legacy_docker_flag() {
620 let _lock = lock_env();
621 unsafe { std::env::remove_var("FEATURE_CONTAINER_SANDBOX") };
622 unsafe { std::env::set_var("FEATURE_DOCKER_CAPABILITY", "true") };
623 let flags = InternalFeatureFlags::from_env();
624 assert!(flags.container_sandbox);
625 unsafe { std::env::remove_var("FEATURE_DOCKER_CAPABILITY") };
626 }
627
628 #[test]
629 fn test_internal_is_enabled_dynamic() {
630 let flags = InternalFeatureFlags {
631 docker_capability: true,
632 container_sandbox: true,
633 session_sandbox: true,
634 machine_payments: true,
635 lua: true,
636 };
637 assert!(flags.is_enabled("docker_capability"));
638 assert!(flags.is_enabled("container_sandbox"));
639 assert!(flags.is_enabled("session_sandbox"));
640 assert!(flags.is_enabled("machine_payments"));
641 assert!(flags.is_enabled("lua"));
642 assert!(!flags.is_enabled("nonexistent"));
643 }
644
645 #[test]
646 fn test_machine_payments_disabled_by_default() {
647 let _lock = lock_env();
648 let prev = std::env::var("FEATURE_MACHINE_PAYMENTS").ok();
649 unsafe { std::env::remove_var("FEATURE_MACHINE_PAYMENTS") };
650 let flags = InternalFeatureFlags::from_env();
651 assert!(
652 !flags.machine_payments,
653 "machine_payments should be disabled by default on all envs"
654 );
655 restore_env("FEATURE_MACHINE_PAYMENTS", prev);
656 }
657
658 #[test]
659 fn test_machine_payments_enabled_by_env_override() {
660 let _lock = lock_env();
661 let prev = std::env::var("FEATURE_MACHINE_PAYMENTS").ok();
662 unsafe { std::env::set_var("FEATURE_MACHINE_PAYMENTS", "true") };
663 let flags = InternalFeatureFlags::from_env();
664 assert!(flags.machine_payments);
665 restore_env("FEATURE_MACHINE_PAYMENTS", prev);
666 }
667
668 #[test]
669 fn test_session_sandbox_flag_enabled_by_env_override() {
670 let _lock = lock_env();
671 unsafe { std::env::set_var("FEATURE_SESSION_SANDBOX", "true") };
672 let flags = InternalFeatureFlags::from_env();
673 assert!(flags.session_sandbox);
674 unsafe { std::env::remove_var("FEATURE_SESSION_SANDBOX") };
675 }
676}