Compare commits

..

1 Commits

Author SHA1 Message Date
Channing Conger
eb1b0a18f4 Introduce code-mode backend seam
Route core code-mode execution through a host-owned backend abstraction so a future subprocess client can replace the current in-process runtime without changing turn orchestration.
Keep the runtime crate reusable for direct-link consumers and move local stored_values ownership into core ahead of the process split.
2026-05-06 17:55:57 -07:00
7 changed files with 124 additions and 290 deletions

View File

@@ -90,8 +90,6 @@ pub struct ConfigRequirements {
pub managed_hooks: Option<ConstrainedWithSource<ManagedHooksRequirementsToml>>,
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
pub plugins: Option<Sourced<BTreeMap<String, PluginRequirementsToml>>>,
pub skills: Option<Sourced<SkillsRequirementsToml>>,
pub plugin_marketplaces: Option<Sourced<PluginMarketplaceRequirementsToml>>,
pub exec_policy: Option<Sourced<RequirementsExecPolicy>>,
pub enforce_residency: ConstrainedWithSource<Option<ResidencyRequirement>>,
/// Managed network constraints derived from requirements.
@@ -125,8 +123,6 @@ impl Default for ConfigRequirements {
managed_hooks: None,
mcp_servers: None,
plugins: None,
skills: None,
plugin_marketplaces: None,
exec_policy: None,
enforce_residency: ConstrainedWithSource::new(
Constrained::allow_any(/*initial_value*/ None),
@@ -168,57 +164,6 @@ impl PluginRequirementsToml {
}
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum SkillSourceRequirement {
User,
Repo,
System,
Admin,
Plugin,
}
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct SkillsRequirementsToml {
pub allowed_sources: Option<Vec<SkillSourceRequirement>>,
}
impl SkillsRequirementsToml {
pub fn is_empty(&self) -> bool {
self.allowed_sources.is_none()
}
pub fn allows_source(&self, source: SkillSourceRequirement) -> bool {
self.allowed_sources
.as_ref()
.is_none_or(|sources| sources.contains(&source))
}
}
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct PluginMarketplaceRequirementsToml {
pub allowed_names: Option<Vec<String>>,
pub allow_user_additions: Option<bool>,
}
impl PluginMarketplaceRequirementsToml {
pub fn is_empty(&self) -> bool {
self.allowed_names.is_none() && self.allow_user_additions.is_none()
}
pub fn allows_marketplace(&self, marketplace_name: &str) -> bool {
self.allowed_names.as_ref().is_none_or(|allowed_names| {
allowed_names
.iter()
.any(|allowed_name| allowed_name == marketplace_name)
})
}
pub fn allows_user_additions(&self) -> bool {
self.allow_user_additions.unwrap_or(true)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct NetworkDomainPermissionsToml {
#[serde(flatten)]
@@ -749,8 +694,6 @@ pub struct ConfigRequirementsWithSources {
pub hooks: Option<Sourced<ManagedHooksRequirementsToml>>,
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
pub plugins: Option<Sourced<BTreeMap<String, PluginRequirementsToml>>>,
pub skills: Option<Sourced<SkillsRequirementsToml>>,
pub plugin_marketplaces: Option<Sourced<PluginMarketplaceRequirementsToml>>,
pub apps: Option<Sourced<AppsRequirementsToml>>,
pub rules: Option<Sourced<RequirementsExecPolicyToml>>,
pub enforce_residency: Option<Sourced<ResidencyRequirement>>,
@@ -843,8 +786,6 @@ impl ConfigRequirementsWithSources {
hooks,
mcp_servers,
plugins,
skills: _,
plugin_marketplaces: _,
apps,
rules,
enforce_residency,
@@ -982,8 +923,6 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
hooks,
mcp_servers,
plugins,
skills,
plugin_marketplaces,
apps: _apps,
rules,
enforce_residency,
@@ -1219,8 +1158,6 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
managed_hooks,
mcp_servers,
plugins,
skills,
plugin_marketplaces,
exec_policy,
enforce_residency,
network,
@@ -1314,8 +1251,6 @@ mod tests {
hooks: hooks.map(|value| Sourced::new(value, RequirementSource::Unknown)),
mcp_servers: mcp_servers.map(|value| Sourced::new(value, RequirementSource::Unknown)),
plugins: plugins.map(|value| Sourced::new(value, RequirementSource::Unknown)),
skills: None,
plugin_marketplaces: None,
apps: apps.map(|value| Sourced::new(value, RequirementSource::Unknown)),
rules: rules.map(|value| Sourced::new(value, RequirementSource::Unknown)),
enforce_residency: enforce_residency
@@ -1395,8 +1330,6 @@ mod tests {
hooks: None,
mcp_servers: None,
plugins: None,
skills: None,
plugin_marketplaces: None,
apps: None,
rules: None,
enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)),
@@ -1436,8 +1369,6 @@ mod tests {
hooks: None,
mcp_servers: None,
plugins: None,
skills: None,
plugin_marketplaces: None,
apps: None,
rules: None,
enforce_residency: None,
@@ -1485,8 +1416,6 @@ mod tests {
hooks: None,
mcp_servers: None,
plugins: None,
skills: None,
plugin_marketplaces: None,
apps: None,
rules: None,
enforce_residency: None,

View File

@@ -49,14 +49,11 @@ pub use config_requirements::NetworkDomainPermissionsToml;
pub use config_requirements::NetworkRequirementsToml;
pub use config_requirements::NetworkUnixSocketPermissionToml;
pub use config_requirements::NetworkUnixSocketPermissionsToml;
pub use config_requirements::PluginMarketplaceRequirementsToml;
pub use config_requirements::PluginRequirementsToml;
pub use config_requirements::RemoteSandboxConfigToml;
pub use config_requirements::RequirementSource;
pub use config_requirements::ResidencyRequirement;
pub use config_requirements::SandboxModeRequirement;
pub use config_requirements::SkillSourceRequirement;
pub use config_requirements::SkillsRequirementsToml;
pub use config_requirements::Sourced;
pub use config_requirements::WebSearchModeRequirement;
pub use config_requirements::sandbox_mode_requirement_for_permission_profile;

View File

@@ -4,7 +4,6 @@ use std::sync::Arc;
use std::sync::RwLock;
use codex_config::ConfigLayerStack;
use codex_config::SkillSourceRequirement;
use codex_exec_server::ExecutorFileSystem;
use codex_protocol::protocol::Product;
use codex_protocol::protocol::SkillScope;
@@ -52,7 +51,7 @@ impl SkillsLoadInput {
pub struct SkillsManager {
codex_home: AbsolutePathBuf,
restriction_product: Option<Product>,
cache_by_cwd: RwLock<HashMap<CwdSkillsCacheKey, SkillLoadOutcome>>,
cache_by_cwd: RwLock<HashMap<AbsolutePathBuf, SkillLoadOutcome>>,
cache_by_config: RwLock<HashMap<ConfigSkillsCacheKey, SkillLoadOutcome>>,
}
@@ -124,7 +123,6 @@ impl SkillsManager {
if !input.bundled_skills_enabled {
roots.retain(|root| root.scope != SkillScope::System);
}
retain_allowed_skill_roots(&mut roots, &input.config_layer_stack);
roots
}
@@ -146,10 +144,9 @@ impl SkillsManager {
fs: Option<Arc<dyn ExecutorFileSystem>>,
) -> SkillLoadOutcome {
let use_cwd_cache = fs.is_some();
let cache_key = cwd_skills_cache_key(&input.cwd, &input.config_layer_stack);
if use_cwd_cache
&& !force_reload
&& let Some(outcome) = self.cached_outcome_for_cwd(&cache_key)
&& let Some(outcome) = self.cached_outcome_for_cwd(&input.cwd)
{
return outcome;
}
@@ -176,7 +173,6 @@ impl SkillsManager {
}),
);
}
retain_allowed_skill_roots(&mut roots, &input.config_layer_stack);
let skill_config_rules = skill_config_rules_from_stack(&input.config_layer_stack);
let outcome = self.build_skill_outcome(roots, &skill_config_rules).await;
if use_cwd_cache {
@@ -184,7 +180,7 @@ impl SkillsManager {
.cache_by_cwd
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
cache.insert(cache_key, outcome.clone());
cache.insert(input.cwd.clone(), outcome.clone());
}
outcome
}
@@ -225,10 +221,10 @@ impl SkillsManager {
info!("skills cache cleared ({cleared} entries)");
}
fn cached_outcome_for_cwd(&self, cache_key: &CwdSkillsCacheKey) -> Option<SkillLoadOutcome> {
fn cached_outcome_for_cwd(&self, cwd: &AbsolutePathBuf) -> Option<SkillLoadOutcome> {
match self.cache_by_cwd.read() {
Ok(cache) => cache.get(cache_key).cloned(),
Err(err) => err.into_inner().get(cache_key).cloned(),
Ok(cache) => cache.get(cwd).cloned(),
Err(err) => err.into_inner().get(cwd).cloned(),
}
}
@@ -243,43 +239,12 @@ impl SkillsManager {
}
}
fn retain_allowed_skill_roots(roots: &mut Vec<SkillRoot>, config_layer_stack: &ConfigLayerStack) {
let Some(requirements) = config_layer_stack.requirements().skills.as_ref() else {
return;
};
roots.retain(|root| {
requirements
.value
.allows_source(skill_source_for_root(root))
});
}
fn skill_source_for_root(root: &SkillRoot) -> SkillSourceRequirement {
if root.plugin_id.is_some() {
return SkillSourceRequirement::Plugin;
}
match root.scope {
SkillScope::Repo => SkillSourceRequirement::Repo,
SkillScope::User => SkillSourceRequirement::User,
SkillScope::System => SkillSourceRequirement::System,
SkillScope::Admin => SkillSourceRequirement::Admin,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct ConfigSkillsCacheKey {
roots: Vec<(AbsolutePathBuf, u8, Option<String>)>,
skill_config_rules: SkillConfigRules,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct CwdSkillsCacheKey {
cwd: AbsolutePathBuf,
allowed_sources: Option<Vec<SkillSourceRequirement>>,
}
pub fn bundled_skills_enabled_from_stack(
config_layer_stack: &codex_config::ConfigLayerStack,
) -> bool {
@@ -323,20 +288,6 @@ fn config_skills_cache_key(
}
}
fn cwd_skills_cache_key(
cwd: &AbsolutePathBuf,
config_layer_stack: &ConfigLayerStack,
) -> CwdSkillsCacheKey {
CwdSkillsCacheKey {
cwd: cwd.clone(),
allowed_sources: config_layer_stack
.requirements()
.skills
.as_ref()
.and_then(|requirements| requirements.value.allowed_sources.clone()),
}
}
fn finalize_skill_outcome(
mut outcome: SkillLoadOutcome,
disabled_paths: HashSet<AbsolutePathBuf>,

View File

@@ -6,12 +6,7 @@ use codex_app_server_protocol::ConfigLayerSource;
use codex_config::CONFIG_TOML_FILE;
use codex_config::ConfigLayerEntry;
use codex_config::ConfigLayerStack;
use codex_config::ConfigRequirements;
use codex_config::ConfigRequirementsToml;
use codex_config::RequirementSource;
use codex_config::SkillSourceRequirement;
use codex_config::SkillsRequirementsToml;
use codex_config::Sourced;
use codex_exec_server::LOCAL_FS;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::test_support::PathBufExt;
@@ -32,13 +27,6 @@ fn write_user_skill(codex_home: &TempDir, dir: &str, name: &str, description: &s
fs::write(skill_dir.join("SKILL.md"), content).unwrap();
}
fn write_repo_skill(cwd: &TempDir, dir: &str, name: &str, description: &str) {
let skill_dir = cwd.path().join(".agents/skills").join(dir);
fs::create_dir_all(&skill_dir).unwrap();
let content = format!("---\nname: {name}\ndescription: {description}\n---\n\n# Body\n");
fs::write(skill_dir.join("SKILL.md"), content).unwrap();
}
fn write_plugin_skill(
codex_home: &TempDir,
marketplace: &str,
@@ -106,17 +94,9 @@ fn user_config_layer(codex_home: &TempDir, config_toml: &str) -> ConfigLayerEntr
}
fn config_stack(codex_home: &TempDir, user_config_toml: &str) -> ConfigLayerStack {
config_stack_with_requirements(codex_home, user_config_toml, ConfigRequirements::default())
}
fn config_stack_with_requirements(
codex_home: &TempDir,
user_config_toml: &str,
requirements: ConfigRequirements,
) -> ConfigLayerStack {
ConfigLayerStack::new(
vec![user_config_layer(codex_home, user_config_toml)],
requirements,
Default::default(),
ConfigRequirementsToml::default(),
)
.expect("valid config layer stack")
@@ -282,65 +262,6 @@ async fn skills_for_config_disables_plugin_skills_by_name() {
);
}
#[tokio::test]
async fn skills_for_config_filters_disallowed_managed_sources() {
let codex_home = tempfile::tempdir().expect("tempdir");
let cwd = tempfile::tempdir().expect("tempdir");
write_user_skill(&codex_home, "user", "user-skill", "from user");
write_repo_skill(&cwd, "repo", "repo-skill", "from repo");
let plugin_skill_path = write_plugin_skill(
&codex_home,
"test",
"sample",
"plugin",
"plugin-skill",
"from plugin",
);
let config_layer_stack = config_stack_with_requirements(
&codex_home,
"",
ConfigRequirements {
skills: Some(Sourced::new(
SkillsRequirementsToml {
allowed_sources: Some(vec![
SkillSourceRequirement::System,
SkillSourceRequirement::Admin,
SkillSourceRequirement::Plugin,
]),
},
RequirementSource::Unknown,
)),
..Default::default()
},
);
let skills_manager = SkillsManager::new(
codex_home.path().abs(),
/*bundled_skills_enabled*/ true,
);
let outcome = skills_for_config_with_stack(
&skills_manager,
&cwd,
&config_layer_stack,
&[plugin_skill_path
.parent()
.and_then(std::path::Path::parent)
.expect("plugin skills root")
.to_path_buf()
.abs()],
)
.await;
let skill_names = outcome
.skills
.iter()
.map(|skill| skill.name.as_str())
.collect::<HashSet<_>>();
assert!(skill_names.contains("sample:plugin-skill"));
assert!(!skill_names.contains("user-skill"));
assert!(!skill_names.contains("repo-skill"));
}
#[tokio::test]
async fn skills_for_cwd_reuses_cached_entry_even_when_entry_has_extra_roots() {
let codex_home = tempfile::tempdir().expect("tempdir");
@@ -401,71 +322,6 @@ async fn skills_for_cwd_reuses_cached_entry_even_when_entry_has_extra_roots() {
assert_eq!(outcome_without_extra.errors, outcome_with_extra.errors);
}
#[tokio::test]
async fn skills_for_cwd_separates_cache_entries_by_managed_allowed_sources() {
let codex_home = tempfile::tempdir().expect("tempdir");
let cwd = tempfile::tempdir().expect("tempdir");
write_user_skill(&codex_home, "user", "user-skill", "from user");
let unrestricted_stack = config_stack(&codex_home, "");
let restricted_stack = config_stack_with_requirements(
&codex_home,
"",
ConfigRequirements {
skills: Some(Sourced::new(
SkillsRequirementsToml {
allowed_sources: Some(vec![SkillSourceRequirement::System]),
},
RequirementSource::Unknown,
)),
..Default::default()
},
);
let skills_manager = SkillsManager::new(
codex_home.path().abs(),
/*bundled_skills_enabled*/ true,
);
let unrestricted_input = SkillsLoadInput::new(
cwd.path().abs(),
Vec::new(),
unrestricted_stack.clone(),
bundled_skills_enabled_from_stack(&unrestricted_stack),
);
let unrestricted = skills_manager
.skills_for_cwd(
&unrestricted_input,
/*force_reload*/ false,
Some(Arc::clone(&LOCAL_FS)),
)
.await;
assert!(
unrestricted
.skills
.iter()
.any(|skill| skill.name == "user-skill")
);
let restricted_input = SkillsLoadInput::new(
cwd.path().abs(),
Vec::new(),
restricted_stack.clone(),
bundled_skills_enabled_from_stack(&restricted_stack),
);
let restricted = skills_manager
.skills_for_cwd(
&restricted_input,
/*force_reload*/ false,
Some(Arc::clone(&LOCAL_FS)),
)
.await;
assert!(
!restricted
.skills
.iter()
.any(|skill| skill.name == "user-skill")
);
}
#[tokio::test]
async fn skills_for_cwd_loads_repo_user_and_extra_roots_with_local_fs() {
let codex_home = tempfile::tempdir().expect("tempdir");

View File

@@ -2117,8 +2117,6 @@ impl Config {
network: network_requirements,
filesystem: filesystem_requirements,
guardian_policy_config_source: _,
skills: _,
plugin_marketplaces: _,
} = config_layer_stack.requirements().clone();
let user_instructions = AgentsMdManager::load_global_instructions(Some(&codex_home))

View File

@@ -0,0 +1,98 @@
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use codex_code_mode::CodeModeTurnHost;
use codex_code_mode::ExecuteRequest;
use codex_code_mode::RuntimeResponse;
use codex_code_mode::WaitOutcome;
use codex_code_mode::WaitRequest;
use serde_json::Value as JsonValue;
/// Host-facing execution boundary for code-mode runtimes.
///
/// Implementations own how JavaScript execution is reached, while callers keep
/// the model-facing host behavior in `codex-core`. The initial implementation is
/// in-process; later implementations can forward the same requests to a child
/// process without changing the call sites that manage turns.
pub(super) trait CodeModeBackend: Send + Sync {
fn allocate_cell_id(&self) -> String;
fn execute(
&self,
request: ExecuteRequest,
) -> Pin<Box<dyn Future<Output = Result<RuntimeResponse, String>> + Send + '_>>;
fn wait(
&self,
request: WaitRequest,
) -> Pin<Box<dyn Future<Output = Result<WaitOutcome, String>> + Send + '_>>;
fn start_turn_worker(&self, host: Arc<dyn CodeModeTurnHost>) -> CodeModeTurnWorker;
}
/// Opaque turn-scoped worker guard returned by code-mode backends.
///
/// The host only needs to keep this guard alive until the turn ends. Hiding the
/// concrete type keeps the public host seam independent of whether the backend
/// is local or process-backed.
pub(crate) struct CodeModeTurnWorker {
_inner: Box<dyn TurnWorkerHandle>,
}
impl CodeModeTurnWorker {
fn new(inner: impl TurnWorkerHandle + 'static) -> Self {
Self {
_inner: Box::new(inner),
}
}
}
trait TurnWorkerHandle: Send {}
impl<T> TurnWorkerHandle for T where T: Send {}
pub(super) struct InProcessCodeModeBackend {
inner: codex_code_mode::CodeModeService,
}
impl InProcessCodeModeBackend {
pub(super) fn new() -> Self {
Self {
inner: codex_code_mode::CodeModeService::new(),
}
}
}
impl Default for InProcessCodeModeBackend {
fn default() -> Self {
Self::new()
}
}
impl CodeModeBackend for InProcessCodeModeBackend {
fn allocate_cell_id(&self) -> String {
self.inner.allocate_cell_id()
}
fn execute(
&self,
request: ExecuteRequest,
) -> Pin<Box<dyn Future<Output = Result<RuntimeResponse, String>> + Send + '_>> {
Box::pin(self.inner.execute(request))
}
fn wait(
&self,
request: WaitRequest,
) -> Pin<Box<dyn Future<Output = Result<WaitOutcome, String>> + Send + '_>> {
Box::pin(self.inner.wait(request))
}
fn start_turn_worker(&self, host: Arc<dyn CodeModeTurnHost>) -> CodeModeTurnWorker {
CodeModeTurnWorker::new(self.inner.start_turn_worker(host))
}
}
pub(super) type StoredValues = HashMap<String, JsonValue>;

View File

@@ -1,3 +1,4 @@
mod backend;
mod execute_handler;
pub(crate) mod execute_spec;
mod response_adapter;
@@ -8,6 +9,10 @@ use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use backend::CodeModeBackend;
use backend::CodeModeTurnWorker;
use backend::InProcessCodeModeBackend;
use backend::StoredValues;
use codex_code_mode::CodeModeNestedToolCall;
use codex_code_mode::CodeModeTurnHost;
use codex_code_mode::RuntimeResponse;
@@ -15,6 +20,7 @@ use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseInputItem;
use serde_json::Value as JsonValue;
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use crate::function_tool::FunctionCallError;
@@ -59,43 +65,42 @@ pub(crate) struct ExecContext {
}
pub(crate) struct CodeModeService {
inner: codex_code_mode::CodeModeService,
backend: Arc<dyn CodeModeBackend>,
stored_values: Mutex<StoredValues>,
}
impl CodeModeService {
pub(crate) fn new() -> Self {
Self {
inner: codex_code_mode::CodeModeService::new(),
backend: Arc::new(InProcessCodeModeBackend::new()),
stored_values: Mutex::new(StoredValues::new()),
}
}
pub(crate) async fn stored_values(&self) -> std::collections::HashMap<String, JsonValue> {
self.inner.stored_values().await
pub(crate) async fn stored_values(&self) -> StoredValues {
self.stored_values.lock().await.clone()
}
pub(crate) async fn replace_stored_values(
&self,
values: std::collections::HashMap<String, JsonValue>,
) {
self.inner.replace_stored_values(values).await;
pub(crate) async fn replace_stored_values(&self, values: StoredValues) {
*self.stored_values.lock().await = values;
}
pub(crate) fn allocate_cell_id(&self) -> String {
self.inner.allocate_cell_id()
self.backend.allocate_cell_id()
}
pub(crate) async fn execute(
&self,
request: codex_code_mode::ExecuteRequest,
) -> Result<RuntimeResponse, String> {
self.inner.execute(request).await
self.backend.execute(request).await
}
pub(crate) async fn wait(
&self,
request: codex_code_mode::WaitRequest,
) -> Result<codex_code_mode::WaitOutcome, String> {
self.inner.wait(request).await
self.backend.wait(request).await
}
pub(crate) async fn start_turn_worker(
@@ -104,7 +109,7 @@ impl CodeModeService {
turn: &Arc<TurnContext>,
router: Arc<ToolRouter>,
tracker: SharedTurnDiffTracker,
) -> Option<codex_code_mode::CodeModeTurnWorker> {
) -> Option<CodeModeTurnWorker> {
if !turn.features.enabled(Feature::CodeMode) {
return None;
}
@@ -116,7 +121,7 @@ impl CodeModeService {
let tool_runtime =
ToolCallRuntime::new(router, Arc::clone(session), Arc::clone(turn), tracker);
let host = Arc::new(CoreTurnHost { exec, tool_runtime });
Some(self.inner.start_turn_worker(host))
Some(self.backend.start_turn_worker(host))
}
}