mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
bfff0c729f
## Why Enterprises can already constrain approvals, sandboxing, and web search through `requirements.toml` and MDM, but feature flags were still only configurable as managed defaults. That meant an enterprise could suggest feature values, but it could not actually pin them. This change closes that gap and makes enterprise feature requirements behave like the other constrained settings. The effective feature set now stays consistent with enterprise requirements during config load, when config writes are validated, and when runtime code mutates feature flags later in the session. It also tightens the runtime API for managed features. `ManagedFeatures` now follows the same constraint-oriented shape as `Constrained<T>` instead of exposing panic-prone mutation helpers, and production code can no longer construct it through an unconstrained `From<Features>` path. The PR also hardens the `compact_resume_fork` integration coverage on Windows. After the feature-management changes, `compact_resume_after_second_compaction_preserves_history` was overflowing the libtest/Tokio thread stacks on Windows, so the test now uses an explicit larger-stack harness as a pragmatic mitigation. That may not be the ideal root-cause fix, and it merits a parallel investigation into whether part of the async future chain should be boxed to reduce stack pressure instead. ## What Changed Enterprises can now pin feature values in `requirements.toml` with the requirements-side `features` table: ```toml [features] personality = true unified_exec = false ``` Only canonical feature keys are allowed in the requirements `features` table; omitted keys remain unconstrained. - Added a requirements-side pinned feature map to `ConfigRequirementsToml`, threaded it through source-preserving requirements merge and normalization in `codex-config`, and made the TOML surface use `[features]` (while still accepting legacy `[feature_requirements]` for compatibility). - Exposed `featureRequirements` from `configRequirements/read`, regenerated the JSON/TypeScript schema artifacts, and updated the app-server README. - Wrapped the effective feature set in `ManagedFeatures`, backed by `ConstrainedWithSource<Features>`, and changed its API to mirror `Constrained<T>`: `can_set(...)`, `set(...) -> ConstraintResult<()>`, and result-returning `enable` / `disable` / `set_enabled` helpers. - Removed the legacy-usage and bulk-map passthroughs from `ManagedFeatures`; callers that need those behaviors now mutate a plain `Features` value and reapply it through `set(...)`, so the constrained wrapper remains the enforcement boundary. - Removed the production loophole for constructing unconstrained `ManagedFeatures`. Non-test code now creates it through the configured feature-loading path, and `impl From<Features> for ManagedFeatures` is restricted to `#[cfg(test)]`. - Rejected legacy feature aliases in enterprise feature requirements, and return a load error when a pinned combination cannot survive dependency normalization. - Validated config writes against enterprise feature requirements before persisting changes, including explicit conflicting writes and profile-specific feature states that normalize into invalid combinations. - Updated runtime and TUI feature-toggle paths to use the constrained setter API and to persist or apply the effective post-constraint value rather than the requested value. - Updated the `core_test_support` Bazel target to include the bundled core model-catalog fixtures in its runtime data, so helper code that resolves `core/models.json` through runfiles works in remote Bazel test environments. - Renamed the core config test coverage to emphasize that effective feature values are normalized at runtime, while conflicting persisted config writes are rejected. - Ran `compact_resume_after_second_compaction_preserves_history` inside an explicit 8 MiB test thread and Tokio runtime worker stack, following the existing larger-stack integration-test pattern, to keep the Windows `compact_resume_fork` test slice from aborting while a parallel investigation continues into whether some of the underlying async futures should be boxed. ## Verification - `cargo test -p codex-config` - `cargo test -p codex-core feature_requirements_ -- --nocapture` - `cargo test -p codex-core load_requirements_toml_produces_expected_constraints -- --nocapture` - `cargo test -p codex-core compact_resume_after_second_compaction_preserves_history -- --nocapture` - `cargo test -p codex-core compact_resume_fork -- --nocapture` - Re-ran the built `codex-core` `tests/all` binary with `RUST_MIN_STACK=262144` for `compact_resume_after_second_compaction_preserves_history` to confirm the explicit-stack harness fixes the deterministic low-stack repro. - `cargo test -p codex-core` - This still fails locally in unrelated integration areas that expect the `codex` / `test_stdio_server` binaries or hit existing `search_tool` wiremock mismatches. ## Docs `developers.openai.com/codex` should document the requirements-side `[features]` table for enterprise and MDM-managed configuration, including that it only accepts canonical feature keys and that conflicting config writes are rejected.
877 lines
28 KiB
Rust
877 lines
28 KiB
Rust
//! Centralized feature flags and metadata.
|
|
//!
|
|
//! This module defines a small set of toggles that gate experimental and
|
|
//! optional behavior across the codebase. Instead of wiring individual
|
|
//! booleans through multiple types, call sites consult a single `Features`
|
|
//! container attached to `Config`.
|
|
|
|
use crate::config::Config;
|
|
use crate::config::ConfigToml;
|
|
use crate::config::profile::ConfigProfile;
|
|
use crate::protocol::Event;
|
|
use crate::protocol::EventMsg;
|
|
use crate::protocol::WarningEvent;
|
|
use codex_config::CONFIG_TOML_FILE;
|
|
use codex_otel::OtelManager;
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use std::collections::BTreeMap;
|
|
use std::collections::BTreeSet;
|
|
use toml::Value as TomlValue;
|
|
|
|
mod legacy;
|
|
pub(crate) use legacy::LegacyFeatureToggles;
|
|
pub(crate) use legacy::legacy_feature_keys;
|
|
|
|
/// High-level lifecycle stage for a feature.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Stage {
|
|
/// Features that are still under development, not ready for external use
|
|
UnderDevelopment,
|
|
/// Experimental features made available to users through the `/experimental` menu
|
|
Experimental {
|
|
name: &'static str,
|
|
menu_description: &'static str,
|
|
announcement: &'static str,
|
|
},
|
|
/// Stable features. The feature flag is kept for ad-hoc enabling/disabling
|
|
Stable,
|
|
/// Deprecated feature that should not be used anymore.
|
|
Deprecated,
|
|
/// The feature flag is useless but kept for backward compatibility reason.
|
|
Removed,
|
|
}
|
|
|
|
impl Stage {
|
|
pub fn experimental_menu_name(self) -> Option<&'static str> {
|
|
match self {
|
|
Stage::Experimental { name, .. } => Some(name),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn experimental_menu_description(self) -> Option<&'static str> {
|
|
match self {
|
|
Stage::Experimental {
|
|
menu_description, ..
|
|
} => Some(menu_description),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn experimental_announcement(self) -> Option<&'static str> {
|
|
match self {
|
|
Stage::Experimental { announcement, .. } => Some(announcement),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Unique features toggled via configuration.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
pub enum Feature {
|
|
// Stable.
|
|
/// Create a ghost commit at each turn.
|
|
GhostCommit,
|
|
/// Enable the default shell tool.
|
|
ShellTool,
|
|
|
|
// Experimental
|
|
/// Enable JavaScript REPL tools backed by a persistent Node kernel.
|
|
JsRepl,
|
|
/// Only expose js_repl tools directly to the model.
|
|
JsReplToolsOnly,
|
|
/// Use the single unified PTY-backed exec tool.
|
|
UnifiedExec,
|
|
/// Route shell tool execution through the zsh exec bridge.
|
|
ShellZshFork,
|
|
/// Include the freeform apply_patch tool.
|
|
ApplyPatchFreeform,
|
|
/// Allow requesting additional filesystem permissions while staying sandboxed.
|
|
RequestPermissions,
|
|
/// Allow the model to request web searches that fetch live content.
|
|
WebSearchRequest,
|
|
/// Allow the model to request web searches that fetch cached content.
|
|
/// Takes precedence over `WebSearchRequest`.
|
|
WebSearchCached,
|
|
/// Legacy search-tool feature flag kept for backward compatibility.
|
|
SearchTool,
|
|
/// Use the bubblewrap-based Linux sandbox pipeline.
|
|
UseLinuxSandboxBwrap,
|
|
/// Allow the model to request approval and propose exec rules.
|
|
RequestRule,
|
|
/// Enable Windows sandbox (restricted token) on Windows.
|
|
WindowsSandbox,
|
|
/// Use the elevated Windows sandbox pipeline (setup + runner).
|
|
WindowsSandboxElevated,
|
|
/// Legacy remote models flag kept for backward compatibility.
|
|
RemoteModels,
|
|
/// Experimental shell snapshotting.
|
|
ShellSnapshot,
|
|
/// Enable git commit attribution guidance via model instructions.
|
|
CodexGitCommit,
|
|
/// Enable runtime metrics snapshots via a manual reader.
|
|
RuntimeMetrics,
|
|
/// Persist rollout metadata to a local SQLite database.
|
|
Sqlite,
|
|
/// Enable startup memory extraction and file-backed memory consolidation.
|
|
MemoryTool,
|
|
/// Append additional AGENTS.md guidance to user instructions.
|
|
ChildAgentsMd,
|
|
/// Allow `detail: "original"` image outputs on supported models.
|
|
ImageDetailOriginal,
|
|
/// Enforce UTF8 output in Powershell.
|
|
PowershellUtf8,
|
|
/// Compress request bodies (zstd) when sending streaming requests to codex-backend.
|
|
EnableRequestCompression,
|
|
/// Enable collab tools.
|
|
Collab,
|
|
/// Enable apps.
|
|
Apps,
|
|
/// Enable plugins.
|
|
Plugins,
|
|
/// Route apps MCP calls through the configured gateway.
|
|
AppsMcpGateway,
|
|
/// Allow prompting and installing missing MCP dependencies.
|
|
SkillMcpDependencyInstall,
|
|
/// Prompt for missing skill env var dependencies.
|
|
SkillEnvVarDependencyPrompt,
|
|
/// Steer feature flag - when enabled, Enter submits immediately instead of queuing.
|
|
/// Kept for config backward compatibility; behavior is always steer-enabled.
|
|
Steer,
|
|
/// Allow request_user_input in Default collaboration mode.
|
|
DefaultModeRequestUserInput,
|
|
/// Enable collaboration modes (Plan, Default).
|
|
/// Kept for config backward compatibility; behavior is always collaboration-modes-enabled.
|
|
CollaborationModes,
|
|
/// Enable personality selection in the TUI.
|
|
Personality,
|
|
/// Enable native artifact tools.
|
|
Artifact,
|
|
/// Enable Fast mode selection in the TUI and request layer.
|
|
FastMode,
|
|
/// Enable voice transcription in the TUI composer.
|
|
VoiceTranscription,
|
|
/// Enable experimental realtime voice conversation mode in the TUI.
|
|
RealtimeConversation,
|
|
/// Prevent idle system sleep while a turn is actively running.
|
|
PreventIdleSleep,
|
|
/// Use the Responses API WebSocket transport for OpenAI by default.
|
|
ResponsesWebsockets,
|
|
/// Enable Responses API websocket v2 mode.
|
|
ResponsesWebsocketsV2,
|
|
}
|
|
|
|
impl Feature {
|
|
pub fn key(self) -> &'static str {
|
|
self.info().key
|
|
}
|
|
|
|
pub fn stage(self) -> Stage {
|
|
self.info().stage
|
|
}
|
|
|
|
pub fn default_enabled(self) -> bool {
|
|
self.info().default_enabled
|
|
}
|
|
|
|
fn info(self) -> &'static FeatureSpec {
|
|
FEATURES
|
|
.iter()
|
|
.find(|spec| spec.id == self)
|
|
.unwrap_or_else(|| unreachable!("missing FeatureSpec for {:?}", self))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct LegacyFeatureUsage {
|
|
pub alias: String,
|
|
pub feature: Feature,
|
|
pub summary: String,
|
|
pub details: Option<String>,
|
|
}
|
|
|
|
/// Holds the effective set of enabled features.
|
|
#[derive(Debug, Clone, Default, PartialEq)]
|
|
pub struct Features {
|
|
enabled: BTreeSet<Feature>,
|
|
legacy_usages: BTreeSet<LegacyFeatureUsage>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct FeatureOverrides {
|
|
pub include_apply_patch_tool: Option<bool>,
|
|
pub web_search_request: Option<bool>,
|
|
}
|
|
|
|
impl FeatureOverrides {
|
|
fn apply(self, features: &mut Features) {
|
|
LegacyFeatureToggles {
|
|
include_apply_patch_tool: self.include_apply_patch_tool,
|
|
tools_web_search: self.web_search_request,
|
|
..Default::default()
|
|
}
|
|
.apply(features);
|
|
}
|
|
}
|
|
|
|
impl Features {
|
|
/// Starts with built-in defaults.
|
|
pub fn with_defaults() -> Self {
|
|
let mut set = BTreeSet::new();
|
|
for spec in FEATURES {
|
|
if spec.default_enabled {
|
|
set.insert(spec.id);
|
|
}
|
|
}
|
|
Self {
|
|
enabled: set,
|
|
legacy_usages: BTreeSet::new(),
|
|
}
|
|
}
|
|
|
|
pub fn enabled(&self, f: Feature) -> bool {
|
|
self.enabled.contains(&f)
|
|
}
|
|
|
|
pub fn enable(&mut self, f: Feature) -> &mut Self {
|
|
self.enabled.insert(f);
|
|
self
|
|
}
|
|
|
|
pub fn disable(&mut self, f: Feature) -> &mut Self {
|
|
self.enabled.remove(&f);
|
|
self
|
|
}
|
|
|
|
pub fn set_enabled(&mut self, f: Feature, enabled: bool) -> &mut Self {
|
|
if enabled {
|
|
self.enable(f)
|
|
} else {
|
|
self.disable(f)
|
|
}
|
|
}
|
|
|
|
pub fn record_legacy_usage_force(&mut self, alias: &str, feature: Feature) {
|
|
let (summary, details) = legacy_usage_notice(alias, feature);
|
|
self.legacy_usages.insert(LegacyFeatureUsage {
|
|
alias: alias.to_string(),
|
|
feature,
|
|
summary,
|
|
details,
|
|
});
|
|
}
|
|
|
|
pub fn record_legacy_usage(&mut self, alias: &str, feature: Feature) {
|
|
if alias == feature.key() {
|
|
return;
|
|
}
|
|
self.record_legacy_usage_force(alias, feature);
|
|
}
|
|
|
|
pub fn legacy_feature_usages(&self) -> impl Iterator<Item = &LegacyFeatureUsage> + '_ {
|
|
self.legacy_usages.iter()
|
|
}
|
|
|
|
pub fn emit_metrics(&self, otel: &OtelManager) {
|
|
for feature in FEATURES {
|
|
if matches!(feature.stage, Stage::Removed) {
|
|
continue;
|
|
}
|
|
if self.enabled(feature.id) != feature.default_enabled {
|
|
otel.counter(
|
|
"codex.feature.state",
|
|
1,
|
|
&[
|
|
("feature", feature.key),
|
|
("value", &self.enabled(feature.id).to_string()),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Apply a table of key -> bool toggles (e.g. from TOML).
|
|
pub fn apply_map(&mut self, m: &BTreeMap<String, bool>) {
|
|
for (k, v) in m {
|
|
match k.as_str() {
|
|
"web_search_request" => {
|
|
self.record_legacy_usage_force(
|
|
"features.web_search_request",
|
|
Feature::WebSearchRequest,
|
|
);
|
|
}
|
|
"web_search_cached" => {
|
|
self.record_legacy_usage_force(
|
|
"features.web_search_cached",
|
|
Feature::WebSearchCached,
|
|
);
|
|
}
|
|
_ => {}
|
|
}
|
|
match feature_for_key(k) {
|
|
Some(feat) => {
|
|
if k != feat.key() {
|
|
self.record_legacy_usage(k.as_str(), feat);
|
|
}
|
|
if *v {
|
|
self.enable(feat);
|
|
} else {
|
|
self.disable(feat);
|
|
}
|
|
}
|
|
None => {
|
|
tracing::warn!("unknown feature key in config: {k}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn from_config(
|
|
cfg: &ConfigToml,
|
|
config_profile: &ConfigProfile,
|
|
overrides: FeatureOverrides,
|
|
) -> Self {
|
|
let mut features = Features::with_defaults();
|
|
|
|
let base_legacy = LegacyFeatureToggles {
|
|
experimental_use_freeform_apply_patch: cfg.experimental_use_freeform_apply_patch,
|
|
experimental_use_unified_exec_tool: cfg.experimental_use_unified_exec_tool,
|
|
tools_web_search: cfg.tools.as_ref().and_then(|t| t.web_search),
|
|
..Default::default()
|
|
};
|
|
base_legacy.apply(&mut features);
|
|
|
|
if let Some(base_features) = cfg.features.as_ref() {
|
|
features.apply_map(&base_features.entries);
|
|
}
|
|
|
|
let profile_legacy = LegacyFeatureToggles {
|
|
include_apply_patch_tool: config_profile.include_apply_patch_tool,
|
|
experimental_use_freeform_apply_patch: config_profile
|
|
.experimental_use_freeform_apply_patch,
|
|
|
|
experimental_use_unified_exec_tool: config_profile.experimental_use_unified_exec_tool,
|
|
tools_web_search: config_profile.tools_web_search,
|
|
};
|
|
profile_legacy.apply(&mut features);
|
|
if let Some(profile_features) = config_profile.features.as_ref() {
|
|
features.apply_map(&profile_features.entries);
|
|
}
|
|
|
|
overrides.apply(&mut features);
|
|
features.normalize_dependencies();
|
|
|
|
features
|
|
}
|
|
|
|
pub fn enabled_features(&self) -> Vec<Feature> {
|
|
self.enabled.iter().copied().collect()
|
|
}
|
|
|
|
pub(crate) fn normalize_dependencies(&mut self) {
|
|
if self.enabled(Feature::JsReplToolsOnly) && !self.enabled(Feature::JsRepl) {
|
|
tracing::warn!("js_repl_tools_only requires js_repl; disabling js_repl_tools_only");
|
|
self.disable(Feature::JsReplToolsOnly);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn legacy_usage_notice(alias: &str, feature: Feature) -> (String, Option<String>) {
|
|
let canonical = feature.key();
|
|
match feature {
|
|
Feature::WebSearchRequest | Feature::WebSearchCached => {
|
|
let label = match alias {
|
|
"web_search" => "[features].web_search",
|
|
"tools.web_search" => "[tools].web_search",
|
|
"features.web_search_request" | "web_search_request" => {
|
|
"[features].web_search_request"
|
|
}
|
|
"features.web_search_cached" | "web_search_cached" => {
|
|
"[features].web_search_cached"
|
|
}
|
|
_ => alias,
|
|
};
|
|
let summary =
|
|
format!("`{label}` is deprecated because web search is enabled by default.");
|
|
(summary, Some(web_search_details().to_string()))
|
|
}
|
|
_ => {
|
|
let summary = format!("`{alias}` is deprecated. Use `[features].{canonical}` instead.");
|
|
let details = if alias == canonical {
|
|
None
|
|
} else {
|
|
Some(format!(
|
|
"Enable it with `--enable {canonical}` or `[features].{canonical}` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."
|
|
))
|
|
};
|
|
(summary, details)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn web_search_details() -> &'static str {
|
|
"Set `web_search` to `\"live\"`, `\"cached\"`, or `\"disabled\"` at the top level (or under a profile) in config.toml if you want to override it."
|
|
}
|
|
|
|
/// Keys accepted in `[features]` tables.
|
|
pub(crate) fn feature_for_key(key: &str) -> Option<Feature> {
|
|
for spec in FEATURES {
|
|
if spec.key == key {
|
|
return Some(spec.id);
|
|
}
|
|
}
|
|
legacy::feature_for_key(key)
|
|
}
|
|
|
|
pub(crate) fn canonical_feature_for_key(key: &str) -> Option<Feature> {
|
|
FEATURES
|
|
.iter()
|
|
.find(|spec| spec.key == key)
|
|
.map(|spec| spec.id)
|
|
}
|
|
|
|
/// Returns `true` if the provided string matches a known feature toggle key.
|
|
pub fn is_known_feature_key(key: &str) -> bool {
|
|
feature_for_key(key).is_some()
|
|
}
|
|
|
|
/// Deserializable features table for TOML.
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
|
|
pub struct FeaturesToml {
|
|
#[serde(flatten)]
|
|
pub entries: BTreeMap<String, bool>,
|
|
}
|
|
|
|
/// Single, easy-to-read registry of all feature definitions.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct FeatureSpec {
|
|
pub id: Feature,
|
|
pub key: &'static str,
|
|
pub stage: Stage,
|
|
pub default_enabled: bool,
|
|
}
|
|
|
|
pub const FEATURES: &[FeatureSpec] = &[
|
|
// Stable features.
|
|
FeatureSpec {
|
|
id: Feature::GhostCommit,
|
|
key: "undo",
|
|
stage: Stage::Stable,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::ShellTool,
|
|
key: "shell_tool",
|
|
stage: Stage::Stable,
|
|
default_enabled: true,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::UnifiedExec,
|
|
key: "unified_exec",
|
|
stage: Stage::Stable,
|
|
default_enabled: !cfg!(windows),
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::ShellZshFork,
|
|
key: "shell_zsh_fork",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::ShellSnapshot,
|
|
key: "shell_snapshot",
|
|
stage: Stage::Stable,
|
|
default_enabled: true,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::JsRepl,
|
|
key: "js_repl",
|
|
stage: Stage::Experimental {
|
|
name: "JavaScript REPL",
|
|
menu_description: "Enable a persistent Node-backed JavaScript REPL for interactive website debugging and other inline JavaScript execution capabilities. Requires Node >= v22.22.0 installed.",
|
|
announcement: "NEW: JavaScript REPL is now available in /experimental. Enable it, then start a new chat or restart Codex to use it.",
|
|
},
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::JsReplToolsOnly,
|
|
key: "js_repl_tools_only",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::WebSearchRequest,
|
|
key: "web_search_request",
|
|
stage: Stage::Deprecated,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::WebSearchCached,
|
|
key: "web_search_cached",
|
|
stage: Stage::Deprecated,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::SearchTool,
|
|
key: "search_tool",
|
|
stage: Stage::Removed,
|
|
default_enabled: false,
|
|
},
|
|
// Experimental program. Rendered in the `/experimental` menu for users.
|
|
FeatureSpec {
|
|
id: Feature::CodexGitCommit,
|
|
key: "codex_git_commit",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::RuntimeMetrics,
|
|
key: "runtime_metrics",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::Sqlite,
|
|
key: "sqlite",
|
|
stage: Stage::Stable,
|
|
default_enabled: true,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::MemoryTool,
|
|
key: "memories",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::ChildAgentsMd,
|
|
key: "child_agents_md",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::ImageDetailOriginal,
|
|
key: "image_detail_original",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::ApplyPatchFreeform,
|
|
key: "apply_patch_freeform",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::RequestPermissions,
|
|
key: "request_permissions",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::UseLinuxSandboxBwrap,
|
|
key: "use_linux_sandbox_bwrap",
|
|
#[cfg(target_os = "linux")]
|
|
stage: Stage::Experimental {
|
|
name: "Bubblewrap sandbox",
|
|
menu_description: "Try the new linux sandbox based on bubblewrap.",
|
|
announcement: "NEW: Linux bubblewrap sandbox offers stronger filesystem and network controls than Landlock alone, including keeping .git and .codex read-only inside writable workspaces. Enable it in /experimental and restart Codex to try it.",
|
|
},
|
|
#[cfg(not(target_os = "linux"))]
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::RequestRule,
|
|
key: "request_rule",
|
|
stage: Stage::Removed,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::WindowsSandbox,
|
|
key: "experimental_windows_sandbox",
|
|
stage: Stage::Removed,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::WindowsSandboxElevated,
|
|
key: "elevated_windows_sandbox",
|
|
stage: Stage::Removed,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::RemoteModels,
|
|
key: "remote_models",
|
|
stage: Stage::Removed,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::PowershellUtf8,
|
|
key: "powershell_utf8",
|
|
#[cfg(windows)]
|
|
stage: Stage::Stable,
|
|
#[cfg(windows)]
|
|
default_enabled: true,
|
|
#[cfg(not(windows))]
|
|
stage: Stage::UnderDevelopment,
|
|
#[cfg(not(windows))]
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::EnableRequestCompression,
|
|
key: "enable_request_compression",
|
|
stage: Stage::Stable,
|
|
default_enabled: true,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::Collab,
|
|
key: "multi_agent",
|
|
stage: Stage::Experimental {
|
|
name: "Multi-agents",
|
|
menu_description: "Ask Codex to spawn multiple agents to parallelize the work and win in efficiency.",
|
|
announcement: "NEW: Multi-agents can now be spawned by Codex. Enable in /experimental and restart Codex!",
|
|
},
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::Apps,
|
|
key: "apps",
|
|
stage: Stage::Experimental {
|
|
name: "Apps",
|
|
menu_description: "Use a connected ChatGPT App using \"$\". Install Apps via /apps command. Restart Codex after enabling.",
|
|
announcement: "NEW: Use ChatGPT Apps (Connectors) in Codex via $ mentions. Enable in /experimental and restart Codex!",
|
|
},
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::Plugins,
|
|
key: "plugins",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::AppsMcpGateway,
|
|
key: "apps_mcp_gateway",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::SkillMcpDependencyInstall,
|
|
key: "skill_mcp_dependency_install",
|
|
stage: Stage::Stable,
|
|
default_enabled: true,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::SkillEnvVarDependencyPrompt,
|
|
key: "skill_env_var_dependency_prompt",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::Steer,
|
|
key: "steer",
|
|
stage: Stage::Removed,
|
|
default_enabled: true,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::DefaultModeRequestUserInput,
|
|
key: "default_mode_request_user_input",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::CollaborationModes,
|
|
key: "collaboration_modes",
|
|
stage: Stage::Removed,
|
|
default_enabled: true,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::Personality,
|
|
key: "personality",
|
|
stage: Stage::Stable,
|
|
default_enabled: true,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::Artifact,
|
|
key: "artifact",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::FastMode,
|
|
key: "fast_mode",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::VoiceTranscription,
|
|
key: "voice_transcription",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::RealtimeConversation,
|
|
key: "realtime_conversation",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::PreventIdleSleep,
|
|
key: "prevent_idle_sleep",
|
|
stage: if cfg!(any(
|
|
target_os = "macos",
|
|
target_os = "linux",
|
|
target_os = "windows"
|
|
)) {
|
|
Stage::Experimental {
|
|
name: "Prevent sleep while running",
|
|
menu_description: "Keep your computer awake while Codex is running a thread.",
|
|
announcement: "NEW: Prevent sleep while running is now available in /experimental.",
|
|
}
|
|
} else {
|
|
Stage::UnderDevelopment
|
|
},
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::ResponsesWebsockets,
|
|
key: "responses_websockets",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
id: Feature::ResponsesWebsocketsV2,
|
|
key: "responses_websockets_v2",
|
|
stage: Stage::UnderDevelopment,
|
|
default_enabled: false,
|
|
},
|
|
];
|
|
|
|
/// Push a warning event if any under-development features are enabled.
|
|
pub fn maybe_push_unstable_features_warning(
|
|
config: &Config,
|
|
post_session_configured_events: &mut Vec<Event>,
|
|
) {
|
|
if config.suppress_unstable_features_warning {
|
|
return;
|
|
}
|
|
|
|
let mut under_development_feature_keys = Vec::new();
|
|
if let Some(table) = config
|
|
.config_layer_stack
|
|
.effective_config()
|
|
.get("features")
|
|
.and_then(TomlValue::as_table)
|
|
{
|
|
for (key, value) in table {
|
|
if value.as_bool() != Some(true) {
|
|
continue;
|
|
}
|
|
let Some(spec) = FEATURES.iter().find(|spec| spec.key == key.as_str()) else {
|
|
continue;
|
|
};
|
|
if !config.features.enabled(spec.id) {
|
|
continue;
|
|
}
|
|
if matches!(spec.stage, Stage::UnderDevelopment) {
|
|
under_development_feature_keys.push(spec.key.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
if under_development_feature_keys.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let under_development_feature_keys = under_development_feature_keys.join(", ");
|
|
let config_path = config
|
|
.codex_home
|
|
.join(CONFIG_TOML_FILE)
|
|
.display()
|
|
.to_string();
|
|
let message = format!(
|
|
"Under-development features enabled: {under_development_feature_keys}. Under-development features are incomplete and may behave unpredictably. To suppress this warning, set `suppress_unstable_features_warning = true` in {config_path}."
|
|
);
|
|
post_session_configured_events.push(Event {
|
|
id: "".to_owned(),
|
|
msg: EventMsg::Warning(WarningEvent { message }),
|
|
});
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
use pretty_assertions::assert_eq;
|
|
|
|
#[test]
|
|
fn under_development_features_are_disabled_by_default() {
|
|
for spec in FEATURES {
|
|
if matches!(spec.stage, Stage::UnderDevelopment) {
|
|
assert_eq!(
|
|
spec.default_enabled, false,
|
|
"feature `{}` is under development and must be disabled by default",
|
|
spec.key
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn default_enabled_features_are_stable() {
|
|
for spec in FEATURES {
|
|
if spec.default_enabled {
|
|
assert!(
|
|
matches!(spec.stage, Stage::Stable | Stage::Removed),
|
|
"feature `{}` is enabled by default but is not stable/removed ({:?})",
|
|
spec.key,
|
|
spec.stage
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
fn use_linux_sandbox_bwrap_is_experimental_on_linux() {
|
|
assert!(matches!(
|
|
Feature::UseLinuxSandboxBwrap.stage(),
|
|
Stage::Experimental { .. }
|
|
));
|
|
assert_eq!(Feature::UseLinuxSandboxBwrap.default_enabled(), false);
|
|
}
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
#[test]
|
|
fn use_linux_sandbox_bwrap_is_under_development_off_linux() {
|
|
assert_eq!(
|
|
Feature::UseLinuxSandboxBwrap.stage(),
|
|
Stage::UnderDevelopment
|
|
);
|
|
assert_eq!(Feature::UseLinuxSandboxBwrap.default_enabled(), false);
|
|
}
|
|
|
|
#[test]
|
|
fn js_repl_is_experimental_and_user_toggleable() {
|
|
let spec = Feature::JsRepl.info();
|
|
let stage = spec.stage;
|
|
let expected_node_version = include_str!("../../node-version.txt").trim_end();
|
|
|
|
assert!(matches!(stage, Stage::Experimental { .. }));
|
|
assert_eq!(stage.experimental_menu_name(), Some("JavaScript REPL"));
|
|
assert_eq!(
|
|
stage.experimental_menu_description().map(str::to_owned),
|
|
Some(format!(
|
|
"Enable a persistent Node-backed JavaScript REPL for interactive website debugging and other inline JavaScript execution capabilities. Requires Node >= v{expected_node_version} installed."
|
|
))
|
|
);
|
|
assert_eq!(Feature::JsRepl.default_enabled(), false);
|
|
}
|
|
|
|
#[test]
|
|
fn collab_is_legacy_alias_for_multi_agent() {
|
|
assert_eq!(feature_for_key("multi_agent"), Some(Feature::Collab));
|
|
assert_eq!(feature_for_key("collab"), Some(Feature::Collab));
|
|
}
|
|
}
|