mirror of
https://github.com/openai/codex.git
synced 2026-10-26 18:38:47 +00:00
app-server: add in-memory config layer
git-stack-id: aaron/app-server-in-memory-config---4ivokd7wtu-snl git-stack-title: app-server: add in-memory config layer
This commit is contained in:
@@ -435,6 +435,19 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ConfigInMemorySetParams": {
|
||||
"properties": {
|
||||
"config": {
|
||||
"additionalProperties": true,
|
||||
"description": "Full replacement in-memory config layer, shaped like config.toml. An empty object clears the layer.",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"config"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ConfigReadParams": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
@@ -6093,6 +6106,30 @@
|
||||
"title": "Config/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"config/inMemory/set"
|
||||
],
|
||||
"title": "Config/inMemory/setRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ConfigInMemorySetParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Config/inMemory/setRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
+3
-3
@@ -288,8 +288,8 @@
|
||||
"environmentId": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"itemId": {
|
||||
@@ -326,4 +326,4 @@
|
||||
],
|
||||
"title": "PermissionsRequestApprovalParams",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1593,8 +1593,8 @@
|
||||
"environmentId": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"itemId": {
|
||||
@@ -2005,4 +2005,4 @@
|
||||
}
|
||||
],
|
||||
"title": "ServerRequest"
|
||||
}
|
||||
}
|
||||
+64
-3
@@ -2005,6 +2005,30 @@
|
||||
"title": "Config/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"config/inMemory/set"
|
||||
],
|
||||
"title": "Config/inMemory/setRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ConfigInMemorySetParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Config/inMemory/setRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -3786,8 +3810,8 @@
|
||||
"environmentId": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"itemId": {
|
||||
@@ -7530,6 +7554,26 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ConfigInMemorySetParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"config": {
|
||||
"additionalProperties": true,
|
||||
"description": "Full replacement in-memory config layer, shaped like config.toml. An empty object clears the layer.",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"config"
|
||||
],
|
||||
"title": "ConfigInMemorySetParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ConfigInMemorySetResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ConfigInMemorySetResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ConfigLayer": {
|
||||
"properties": {
|
||||
"config": true,
|
||||
@@ -7681,6 +7725,23 @@
|
||||
"title": "UserConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "In-memory config supplied by the host for the current process.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"inMemory"
|
||||
],
|
||||
"title": "InMemoryConfigLayerSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "InMemoryConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Path to a .codex/ folder within a project. There could be multiple of these between `cwd` and the project/repo root.",
|
||||
"properties": {
|
||||
@@ -19239,4 +19300,4 @@
|
||||
},
|
||||
"title": "CodexAppServerProtocol",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
+61
@@ -2764,6 +2764,30 @@
|
||||
"title": "Config/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"config/inMemory/set"
|
||||
],
|
||||
"title": "Config/inMemory/setRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ConfigInMemorySetParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Config/inMemory/setRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -3892,6 +3916,26 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ConfigInMemorySetParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"config": {
|
||||
"additionalProperties": true,
|
||||
"description": "Full replacement in-memory config layer, shaped like config.toml. An empty object clears the layer.",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"config"
|
||||
],
|
||||
"title": "ConfigInMemorySetParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ConfigInMemorySetResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ConfigInMemorySetResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ConfigLayer": {
|
||||
"properties": {
|
||||
"config": true,
|
||||
@@ -4043,6 +4087,23 @@
|
||||
"title": "UserConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "In-memory config supplied by the host for the current process.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"inMemory"
|
||||
],
|
||||
"title": "InMemoryConfigLayerSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "InMemoryConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Path to a .codex/ folder within a project. There could be multiple of these between `cwd` and the project/repo root.",
|
||||
"properties": {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"config": {
|
||||
"additionalProperties": true,
|
||||
"description": "Full replacement in-memory config layer, shaped like config.toml. An empty object clears the layer.",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"config"
|
||||
],
|
||||
"title": "ConfigInMemorySetParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ConfigInMemorySetResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -568,6 +568,23 @@
|
||||
"title": "UserConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "In-memory config supplied by the host for the current process.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"inMemory"
|
||||
],
|
||||
"title": "InMemoryConfigLayerSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "InMemoryConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Path to a .codex/ folder within a project. There could be multiple of these between `cwd` and the project/repo root.",
|
||||
"properties": {
|
||||
|
||||
@@ -133,6 +133,23 @@
|
||||
"title": "UserConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "In-memory config supplied by the host for the current process.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"inMemory"
|
||||
],
|
||||
"title": "InMemoryConfigLayerSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "InMemoryConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Path to a .codex/ folder within a project. There could be multiple of these between `cwd` and the project/repo root.",
|
||||
"properties": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
+11
@@ -0,0 +1,11 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
|
||||
export type ConfigInMemorySetParams = {
|
||||
/**
|
||||
* Full replacement in-memory config layer, shaped like config.toml.
|
||||
* An empty object clears the layer.
|
||||
*/
|
||||
config: { [key in string]?: JsonValue }, };
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ConfigInMemorySetResponse = Record<string, never>;
|
||||
@@ -28,4 +28,4 @@ file: AbsolutePathBuf,
|
||||
* Name of the selected profile-v2 config layered on top of the base
|
||||
* user config, when this layer represents one.
|
||||
*/
|
||||
profile: string | null, } | { "type": "project", dotCodexFolder: AbsolutePathBuf, } | { "type": "sessionFlags" } | { "type": "legacyManagedConfigTomlFromFile", file: AbsolutePathBuf, } | { "type": "legacyManagedConfigTomlFromMdm" };
|
||||
profile: string | null, } | { "type": "inMemory" } | { "type": "project", dotCodexFolder: AbsolutePathBuf, } | { "type": "sessionFlags" } | { "type": "legacyManagedConfigTomlFromFile", file: AbsolutePathBuf, } | { "type": "legacyManagedConfigTomlFromMdm" };
|
||||
|
||||
@@ -68,6 +68,8 @@ export type { ComputerUseRequirements } from "./ComputerUseRequirements";
|
||||
export type { Config } from "./Config";
|
||||
export type { ConfigBatchWriteParams } from "./ConfigBatchWriteParams";
|
||||
export type { ConfigEdit } from "./ConfigEdit";
|
||||
export type { ConfigInMemorySetParams } from "./ConfigInMemorySetParams";
|
||||
export type { ConfigInMemorySetResponse } from "./ConfigInMemorySetResponse";
|
||||
export type { ConfigLayer } from "./ConfigLayer";
|
||||
export type { ConfigLayerMetadata } from "./ConfigLayerMetadata";
|
||||
export type { ConfigLayerSource } from "./ConfigLayerSource";
|
||||
|
||||
@@ -1020,6 +1020,11 @@ client_request_definitions! {
|
||||
serialization: global_shared_read("config"),
|
||||
response: v2::ConfigReadResponse,
|
||||
},
|
||||
ConfigInMemorySet => "config/inMemory/set" {
|
||||
params: v2::ConfigInMemorySetParams,
|
||||
serialization: global("config"),
|
||||
response: v2::ConfigInMemorySetResponse,
|
||||
},
|
||||
ExternalAgentConfigDetect => "externalAgentConfig/detect" {
|
||||
params: v2::ExternalAgentConfigDetectParams,
|
||||
serialization: global("config"),
|
||||
|
||||
@@ -72,6 +72,9 @@ pub enum ConfigLayerSource {
|
||||
profile: Option<String>,
|
||||
},
|
||||
|
||||
/// In-memory config supplied by the host for the current process.
|
||||
InMemory,
|
||||
|
||||
/// Path to a .codex/ folder within a project. There could be multiple of
|
||||
/// these between `cwd` and the project/repo root.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -111,6 +114,7 @@ impl ConfigLayerSource {
|
||||
20
|
||||
}
|
||||
}
|
||||
ConfigLayerSource::InMemory => 22,
|
||||
ConfigLayerSource::Project { .. } => 25,
|
||||
ConfigLayerSource::SessionFlags => 30,
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => 40,
|
||||
@@ -365,6 +369,20 @@ pub struct ConfigReadResponse {
|
||||
pub layers: Option<Vec<ConfigLayer>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ConfigInMemorySetParams {
|
||||
/// Full replacement in-memory config layer, shaped like config.toml.
|
||||
/// An empty object clears the layer.
|
||||
pub config: HashMap<String, JsonValue>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ConfigInMemorySetResponse {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -225,7 +225,8 @@ Example with notification opt-out:
|
||||
- `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result.
|
||||
- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`.
|
||||
- `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id.
|
||||
- `config/read` — fetch the effective config on disk after resolving config layering, including opaque `desktop` values stored in `config.toml`.
|
||||
- `config/read` — fetch the effective config after resolving config layering, including opaque `desktop` values stored in `config.toml`.
|
||||
- `config/inMemory/set` — replace the in-memory config layer used by subsequent config loads in this app-server process. The layer is shaped like `config.toml`, sits above user config and below project/session overrides, and `{ "config": {} }` clears it. Existing running threads retain their config snapshots, just as when `config.toml` changes.
|
||||
- `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome` and optional `cwds`; each detected item includes `cwd` (`null` for home), and plugin/session migration items may additionally include structured `details` grouping plugin ids or session metadata.
|
||||
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any plugin/session `details` returned by detect. When a request includes migration items, the server emits `externalAgentConfig/import/completed` once after the full import finishes (immediately after the response when everything completed synchronously, or after background imports finish).
|
||||
- `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_cloud_config::cloud_config_bundle_loader;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
@@ -29,8 +30,9 @@ pub(crate) struct ConfigManager {
|
||||
codex_home: PathBuf,
|
||||
cli_overrides: Arc<RwLock<Vec<(String, TomlValue)>>>,
|
||||
runtime_feature_enablement: Arc<RwLock<BTreeMap<String, bool>>>,
|
||||
in_memory_layer: Arc<RwLock<Option<ConfigLayerEntry>>>,
|
||||
loader_overrides: LoaderOverrides,
|
||||
strict_config: bool,
|
||||
pub(super) strict_config: bool,
|
||||
cloud_config_bundle: Arc<RwLock<CloudConfigBundleLoader>>,
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
thread_config_loader: Arc<RwLock<Arc<dyn ThreadConfigLoader>>>,
|
||||
@@ -50,6 +52,7 @@ impl ConfigManager {
|
||||
codex_home,
|
||||
cli_overrides: Arc::new(RwLock::new(cli_overrides)),
|
||||
runtime_feature_enablement: Arc::new(RwLock::new(BTreeMap::new())),
|
||||
in_memory_layer: Arc::new(RwLock::new(None)),
|
||||
loader_overrides,
|
||||
strict_config,
|
||||
cloud_config_bundle: Arc::new(RwLock::new(cloud_config_bundle)),
|
||||
@@ -80,6 +83,13 @@ impl ConfigManager {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn current_in_memory_layer(&self) -> std::io::Result<Option<ConfigLayerEntry>> {
|
||||
self.in_memory_layer
|
||||
.read()
|
||||
.map(|guard| guard.clone())
|
||||
.map_err(|_| std::io::Error::other("in-memory config layer lock poisoned"))
|
||||
}
|
||||
|
||||
pub(crate) fn extend_runtime_feature_enablement<I>(&self, enablement: I) -> Result<(), ()>
|
||||
where
|
||||
I: IntoIterator<Item = (String, bool)>,
|
||||
@@ -115,6 +125,18 @@ impl ConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn replace_in_memory_layer(
|
||||
&self,
|
||||
in_memory_layer: Option<ConfigLayerEntry>,
|
||||
) -> std::io::Result<()> {
|
||||
let mut guard = self
|
||||
.in_memory_layer
|
||||
.write()
|
||||
.map_err(|_| std::io::Error::other("in-memory config layer lock poisoned"))?;
|
||||
*guard = in_memory_layer;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn current_thread_config_loader(&self) -> Arc<dyn ThreadConfigLoader> {
|
||||
self.thread_config_loader
|
||||
.read()
|
||||
@@ -246,6 +268,7 @@ impl ConfigManager {
|
||||
.harness_overrides(typesafe_overrides)
|
||||
.fallback_cwd(fallback_cwd)
|
||||
.cloud_config_bundle(self.current_cloud_config_bundle())
|
||||
.in_memory_layer(self.current_in_memory_layer()?)
|
||||
.thread_config_loader(self.current_thread_config_loader())
|
||||
.build()
|
||||
.await?;
|
||||
@@ -264,6 +287,15 @@ impl ConfigManager {
|
||||
pub(crate) async fn load_config_layers(
|
||||
&self,
|
||||
cwd: Option<AbsolutePathBuf>,
|
||||
) -> std::io::Result<ConfigLayerStack> {
|
||||
self.load_config_layers_with_in_memory_layer(cwd, self.current_in_memory_layer()?)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn load_config_layers_with_in_memory_layer(
|
||||
&self,
|
||||
cwd: Option<AbsolutePathBuf>,
|
||||
in_memory_layer: Option<ConfigLayerEntry>,
|
||||
) -> std::io::Result<ConfigLayerStack> {
|
||||
let thread_config_loader = self.current_thread_config_loader();
|
||||
load_config_layers_state(
|
||||
@@ -275,6 +307,7 @@ impl ConfigManager {
|
||||
loader_overrides: self.loader_overrides.clone(),
|
||||
strict_config: self.strict_config,
|
||||
cloud_config_bundle: self.current_cloud_config_bundle(),
|
||||
in_memory_layer,
|
||||
},
|
||||
thread_config_loader.as_ref(),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::config_manager::ConfigManager;
|
||||
use codex_app_server_protocol::Config as ApiConfig;
|
||||
use codex_app_server_protocol::ConfigBatchWriteParams;
|
||||
use codex_app_server_protocol::ConfigInMemorySetParams;
|
||||
use codex_app_server_protocol::ConfigInMemorySetResponse;
|
||||
use codex_app_server_protocol::ConfigLayerMetadata;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_app_server_protocol::ConfigReadParams;
|
||||
@@ -17,6 +19,7 @@ use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::loader::create_in_memory_config_layer;
|
||||
use codex_config::merge_toml_values;
|
||||
use codex_core::config::deserialize_config_toml_with_base;
|
||||
use codex_core::config::edit::ConfigEdit;
|
||||
@@ -189,6 +192,78 @@ impl ConfigManager {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn set_in_memory_config(
|
||||
&self,
|
||||
params: ConfigInMemorySetParams,
|
||||
) -> Result<ConfigInMemorySetResponse, ConfigManagerError> {
|
||||
let ConfigInMemorySetParams { config } = params;
|
||||
let candidate_in_memory_layer = if config.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let config = TomlValue::try_from(config).map_err(|err| {
|
||||
ConfigManagerError::write(
|
||||
ConfigWriteErrorCode::ConfigValidationError,
|
||||
format!("Invalid in-memory configuration: {err}"),
|
||||
)
|
||||
})?;
|
||||
Some(
|
||||
create_in_memory_config_layer(config, self.codex_home(), self.strict_config)
|
||||
.map_err(|err| {
|
||||
ConfigManagerError::write(
|
||||
ConfigWriteErrorCode::ConfigValidationError,
|
||||
format!("Invalid in-memory configuration: {err}"),
|
||||
)
|
||||
})?,
|
||||
)
|
||||
};
|
||||
|
||||
let candidate_layers = self
|
||||
.load_config_layers_with_in_memory_layer(
|
||||
/*cwd*/ None,
|
||||
candidate_in_memory_layer.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ConfigManagerError::io("failed to load configuration", err))?;
|
||||
|
||||
if let Some(layer) = candidate_in_memory_layer.as_ref() {
|
||||
let in_memory_config_toml =
|
||||
deserialize_config_toml_with_base(layer.config.clone(), self.codex_home())
|
||||
.map_err(|err| {
|
||||
ConfigManagerError::write(
|
||||
ConfigWriteErrorCode::ConfigValidationError,
|
||||
format!("Invalid in-memory configuration: {err}"),
|
||||
)
|
||||
})?;
|
||||
validate_feature_requirements_for_config_toml(
|
||||
&in_memory_config_toml,
|
||||
candidate_layers
|
||||
.requirements()
|
||||
.feature_requirements
|
||||
.as_ref(),
|
||||
)
|
||||
.map_err(|err| {
|
||||
ConfigManagerError::write(
|
||||
ConfigWriteErrorCode::ConfigValidationError,
|
||||
format!("Invalid in-memory configuration: {err}"),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
validate_config(&candidate_layers.effective_config()).map_err(|err| {
|
||||
ConfigManagerError::write(
|
||||
ConfigWriteErrorCode::ConfigValidationError,
|
||||
format!("Invalid in-memory configuration: {err}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
self.replace_in_memory_layer(candidate_in_memory_layer)
|
||||
.map_err(|err| {
|
||||
ConfigManagerError::io("failed to update in-memory config layer", err)
|
||||
})?;
|
||||
|
||||
Ok(ConfigInMemorySetResponse {})
|
||||
}
|
||||
|
||||
async fn apply_edits(
|
||||
&self,
|
||||
file_path: Option<String>,
|
||||
@@ -627,6 +702,7 @@ fn override_message(layer: &ConfigLayerSource) -> String {
|
||||
ConfigLayerSource::EnterpriseManaged { id: _, name } => {
|
||||
format!("Overridden by enterprise-managed config: {name}")
|
||||
}
|
||||
ConfigLayerSource::InMemory => "Overridden by in-memory config".to_string(),
|
||||
ConfigLayerSource::Project { dot_codex_folder } => format!(
|
||||
"Overridden by project config: {}/{CONFIG_TOML_FILE}",
|
||||
dot_codex_folder.display(),
|
||||
|
||||
@@ -60,6 +60,79 @@ X-Doc = "42"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn in_memory_config_replaces_atomically_and_clears_to_latest_user_config() -> Result<()> {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let config_path = tmp.path().join(CONFIG_TOML_FILE);
|
||||
std::fs::write(&config_path, "model = \"user\"\n")?;
|
||||
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
|
||||
|
||||
service
|
||||
.set_in_memory_config(ConfigInMemorySetParams {
|
||||
config: std::collections::HashMap::from([(
|
||||
"model".to_string(),
|
||||
serde_json::json!("memory"),
|
||||
)]),
|
||||
})
|
||||
.await
|
||||
.expect("set in-memory config");
|
||||
|
||||
let read = service
|
||||
.read(ConfigReadParams {
|
||||
include_layers: true,
|
||||
cwd: None,
|
||||
})
|
||||
.await
|
||||
.expect("read in-memory config");
|
||||
assert_eq!(read.config.model.as_deref(), Some("memory"));
|
||||
assert_eq!(
|
||||
read.origins.get("model").map(|metadata| &metadata.name),
|
||||
Some(&ConfigLayerSource::InMemory)
|
||||
);
|
||||
|
||||
let error = service
|
||||
.set_in_memory_config(ConfigInMemorySetParams {
|
||||
config: std::collections::HashMap::from([("model".to_string(), serde_json::json!(42))]),
|
||||
})
|
||||
.await
|
||||
.expect_err("invalid replacement should fail");
|
||||
assert_eq!(
|
||||
error.write_error_code(),
|
||||
Some(ConfigWriteErrorCode::ConfigValidationError)
|
||||
);
|
||||
assert_eq!(
|
||||
service
|
||||
.read(ConfigReadParams {
|
||||
include_layers: false,
|
||||
cwd: None,
|
||||
})
|
||||
.await
|
||||
.expect("read after rejected replacement")
|
||||
.config
|
||||
.model
|
||||
.as_deref(),
|
||||
Some("memory")
|
||||
);
|
||||
|
||||
std::fs::write(&config_path, "model = \"latest-user\"\n")?;
|
||||
service
|
||||
.set_in_memory_config(ConfigInMemorySetParams {
|
||||
config: std::collections::HashMap::new(),
|
||||
})
|
||||
.await
|
||||
.expect("clear in-memory config");
|
||||
let cleared = service
|
||||
.read(ConfigReadParams {
|
||||
include_layers: false,
|
||||
cwd: None,
|
||||
})
|
||||
.await
|
||||
.expect("read cleared config");
|
||||
assert_eq!(cleared.config.model.as_deref(), Some("latest-user"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_value_preserves_comments_and_order() -> Result<()> {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
|
||||
@@ -879,6 +879,11 @@ impl MessageProcessor {
|
||||
.read(params)
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::ConfigInMemorySet { params, .. } => self
|
||||
.config_processor
|
||||
.in_memory_set(params)
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::WindowsSandboxReadiness { .. } => self
|
||||
.windows_sandbox_processor
|
||||
.windows_sandbox_readiness()
|
||||
|
||||
@@ -11,6 +11,8 @@ use codex_app_server_protocol::AppListUpdatedNotification;
|
||||
use codex_app_server_protocol::ClientResponsePayload;
|
||||
use codex_app_server_protocol::ComputerUseRequirements;
|
||||
use codex_app_server_protocol::ConfigBatchWriteParams;
|
||||
use codex_app_server_protocol::ConfigInMemorySetParams;
|
||||
use codex_app_server_protocol::ConfigInMemorySetResponse;
|
||||
use codex_app_server_protocol::ConfigReadParams;
|
||||
use codex_app_server_protocol::ConfigReadResponse;
|
||||
use codex_app_server_protocol::ConfigRequirements;
|
||||
@@ -146,6 +148,19 @@ impl ConfigRequestProcessor {
|
||||
.map(ClientResponsePayload::ConfigBatchWrite)
|
||||
}
|
||||
|
||||
pub(crate) async fn in_memory_set(
|
||||
&self,
|
||||
params: ConfigInMemorySetParams,
|
||||
) -> Result<ConfigInMemorySetResponse, JSONRPCErrorError> {
|
||||
self.handle_config_mutation_result(
|
||||
self.config_manager
|
||||
.set_in_memory_config(params)
|
||||
.await
|
||||
.map_err(map_error),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn experimental_feature_enablement_set(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
|
||||
@@ -1076,6 +1076,7 @@ impl TurnRequestProcessor {
|
||||
))
|
||||
})?;
|
||||
|
||||
// Detached review threads intentionally retain the app-server startup config snapshot.
|
||||
let mut config = self.config.as_ref().clone();
|
||||
if let Some(review_model) = &config.review_model {
|
||||
config.model = Some(review_model.clone());
|
||||
|
||||
@@ -14,6 +14,7 @@ pub fn format_config_layer_source(source: &ConfigLayerSource, config_toml_file:
|
||||
ConfigLayerSource::User { file, .. } => {
|
||||
format!("user ({})", file.as_path().display())
|
||||
}
|
||||
ConfigLayerSource::InMemory => "in-memory".to_string(),
|
||||
ConfigLayerSource::Project { dot_codex_folder } => {
|
||||
format!(
|
||||
"project ({}/{config_toml_file})",
|
||||
|
||||
@@ -256,6 +256,7 @@ fn config_path_for_layer(layer: &ConfigLayerEntry, config_toml_file: &str) -> Op
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => Some(file.to_path_buf()),
|
||||
ConfigLayerSource::Mdm { .. }
|
||||
| ConfigLayerSource::EnterpriseManaged { .. }
|
||||
| ConfigLayerSource::InMemory
|
||||
| ConfigLayerSource::SessionFlags
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => None,
|
||||
}
|
||||
|
||||
@@ -29,10 +29,11 @@ Precedence is **top overrides bottom**:
|
||||
2. `LegacyManagedConfigTomlFromFile` (`managed_config.toml`, while it is being phased out)
|
||||
3. `SessionFlags` (CLI overrides, applied as dotted-path TOML writes)
|
||||
4. `Project` config (`.codex/config.toml`)
|
||||
5. `User` profile config, when present
|
||||
6. `User` config (`config.toml`)
|
||||
7. `EnterpriseManaged` cloud-managed config bundle layers
|
||||
8. `System` config (`/etc/codex/config.toml` or the Windows system config path)
|
||||
5. `InMemory` process-scoped config supplied by a host
|
||||
6. `User` profile config, when present
|
||||
7. `User` config (`config.toml`)
|
||||
8. `EnterpriseManaged` cloud-managed config bundle layers
|
||||
9. `System` config (`/etc/codex/config.toml` or the Windows system config path)
|
||||
|
||||
`ConfigLayerStack` stores layers in the opposite order internally: lowest
|
||||
precedence first, highest precedence last, so later layers override earlier
|
||||
|
||||
@@ -97,6 +97,7 @@ async fn first_layer_config_error_from_entries(layers: &[ConfigLayerEntry]) -> O
|
||||
/// - cloud enterprise-managed cloud config bundle fragments
|
||||
/// - user `${CODEX_HOME}/config.toml`
|
||||
/// - profile `${CODEX_HOME}/<name>.config.toml`, when selected
|
||||
/// - in-memory process-scoped config supplied by a host
|
||||
/// - cwd `${PWD}/config.toml` (loaded but disabled when the directory is untrusted)
|
||||
/// - tree parent directories up to root looking for `./.codex/config.toml` (loaded but disabled when untrusted)
|
||||
/// - repo `$(git rev-parse --show-toplevel)/.codex/config.toml` (loaded but disabled when untrusted)
|
||||
@@ -123,6 +124,7 @@ pub async fn load_config_layers_state(
|
||||
loader_overrides: overrides,
|
||||
strict_config,
|
||||
cloud_config_bundle,
|
||||
in_memory_layer,
|
||||
} = options.into();
|
||||
let active_user_profile = overrides.user_config_profile.clone();
|
||||
let ignore_managed_requirements = overrides.ignore_managed_requirements;
|
||||
@@ -207,12 +209,11 @@ pub async fn load_config_layers_state(
|
||||
.as_ref()
|
||||
.map(AbsolutePathBuf::as_path)
|
||||
.unwrap_or(codex_home);
|
||||
if strict_config {
|
||||
validate_cli_overrides_strictly(&cli_overrides_layer, base_dir)?;
|
||||
}
|
||||
Some(resolve_relative_paths_in_config_toml(
|
||||
Some(resolve_inline_config_layer(
|
||||
cli_overrides_layer,
|
||||
base_dir,
|
||||
strict_config,
|
||||
"-c/--config override",
|
||||
)?)
|
||||
};
|
||||
|
||||
@@ -286,6 +287,16 @@ pub async fn load_config_layers_state(
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(in_memory_layer) = in_memory_layer {
|
||||
if in_memory_layer.name != ConfigLayerSource::InMemory {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"in-memory config layer must use the InMemory source",
|
||||
));
|
||||
}
|
||||
layers.push(in_memory_layer);
|
||||
}
|
||||
|
||||
let mut startup_warnings = None;
|
||||
if let Some(cwd) = cwd {
|
||||
let mut merged_so_far = TomlValue::Table(toml::map::Map::new());
|
||||
@@ -537,29 +548,53 @@ fn validate_config_toml_strictly(
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_cli_overrides_strictly(
|
||||
cli_overrides_layer: &TomlValue,
|
||||
fn validate_inline_config_layer_strictly(
|
||||
config_layer: &TomlValue,
|
||||
base_dir: &Path,
|
||||
source_label: &str,
|
||||
) -> io::Result<()> {
|
||||
let _guard = AbsolutePathBufGuard::new(base_dir);
|
||||
if let Some(ignored_path) = ignored_toml_value_field::<ConfigToml>(cli_overrides_layer.clone())
|
||||
{
|
||||
if let Some(ignored_path) = ignored_toml_value_field::<ConfigToml>(config_layer.clone()) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("unknown configuration field `{ignored_path}` in -c/--config override"),
|
||||
format!("unknown configuration field `{ignored_path}` in {source_label}"),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(ignored_path) = unknown_feature_toml_value_field(cli_overrides_layer) {
|
||||
if let Some(ignored_path) = unknown_feature_toml_value_field(config_layer) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("unknown configuration field `{ignored_path}` in -c/--config override"),
|
||||
format!("unknown configuration field `{ignored_path}` in {source_label}"),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_inline_config_layer(
|
||||
config_layer: TomlValue,
|
||||
base_dir: &Path,
|
||||
strict_config: bool,
|
||||
source_label: &str,
|
||||
) -> io::Result<TomlValue> {
|
||||
if strict_config {
|
||||
validate_inline_config_layer_strictly(&config_layer, base_dir, source_label)?;
|
||||
}
|
||||
resolve_relative_paths_in_config_toml(config_layer, base_dir)
|
||||
}
|
||||
|
||||
/// Creates a normalized process-scoped in-memory config layer.
|
||||
#[doc(hidden)]
|
||||
pub fn create_in_memory_config_layer(
|
||||
config: TomlValue,
|
||||
codex_home: &Path,
|
||||
strict_config: bool,
|
||||
) -> io::Result<ConfigLayerEntry> {
|
||||
let config =
|
||||
resolve_inline_config_layer(config, codex_home, strict_config, "in-memory config layer")?;
|
||||
Ok(ConfigLayerEntry::new(ConfigLayerSource::InMemory, config))
|
||||
}
|
||||
|
||||
/// If available, load requirements from the platform's system `requirements.toml`
|
||||
/// location as a requirements layer.
|
||||
pub async fn load_requirements_toml(
|
||||
@@ -1392,6 +1427,7 @@ struct LegacyManagedConfigToml {
|
||||
#[cfg(test)]
|
||||
mod unit_tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
#[cfg(windows)]
|
||||
use std::path::Path;
|
||||
use tempfile::tempdir;
|
||||
@@ -1433,6 +1469,35 @@ foo = "xyzzy"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_memory_config_layer_resolves_paths_against_codex_home() -> anyhow::Result<()> {
|
||||
let codex_home = tempdir()?;
|
||||
let layer = create_in_memory_config_layer(
|
||||
toml::toml! {
|
||||
model_instructions_file = "./instructions.md"
|
||||
}
|
||||
.into(),
|
||||
codex_home.path(),
|
||||
/*strict_config*/ false,
|
||||
)?;
|
||||
let expected = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::InMemory,
|
||||
TomlValue::Table(toml::map::Map::from_iter([(
|
||||
"model_instructions_file".to_string(),
|
||||
TomlValue::String(
|
||||
codex_home
|
||||
.path()
|
||||
.join("instructions.md")
|
||||
.display()
|
||||
.to_string(),
|
||||
),
|
||||
)])),
|
||||
);
|
||||
|
||||
assert_eq!(layer, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_managed_config_backfill_includes_read_only_sandbox_mode() -> io::Result<()> {
|
||||
let legacy = LegacyManagedConfigToml {
|
||||
|
||||
@@ -23,6 +23,7 @@ pub struct ConfigLoadOptions {
|
||||
pub loader_overrides: LoaderOverrides,
|
||||
pub strict_config: bool,
|
||||
pub cloud_config_bundle: CloudConfigBundleLoader,
|
||||
pub in_memory_layer: Option<ConfigLayerEntry>,
|
||||
}
|
||||
|
||||
impl From<LoaderOverrides> for ConfigLoadOptions {
|
||||
@@ -31,6 +32,7 @@ impl From<LoaderOverrides> for ConfigLoadOptions {
|
||||
loader_overrides,
|
||||
strict_config: false,
|
||||
cloud_config_bundle: CloudConfigBundleLoader::default(),
|
||||
in_memory_layer: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,6 +208,7 @@ impl ConfigLayerEntry {
|
||||
ConfigLayerSource::System { file } => file.parent(),
|
||||
ConfigLayerSource::EnterpriseManaged { .. } => None,
|
||||
ConfigLayerSource::User { file, .. } => file.parent(),
|
||||
ConfigLayerSource::InMemory => None,
|
||||
ConfigLayerSource::Project { dot_codex_folder } => Some(dot_codex_folder.clone()),
|
||||
ConfigLayerSource::SessionFlags => None,
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => None,
|
||||
@@ -358,6 +361,35 @@ impl ConfigLayerStack {
|
||||
Some(merged)
|
||||
}
|
||||
|
||||
/// Returns the user-scoped config visible to runtime consumers.
|
||||
///
|
||||
/// This includes the process-scoped in-memory layer. Persistence code
|
||||
/// should continue to use [`Self::effective_user_config`].
|
||||
pub fn effective_runtime_user_config(&self) -> Option<TomlValue> {
|
||||
let layers = self
|
||||
.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false,
|
||||
)
|
||||
.into_iter()
|
||||
.filter(|layer| {
|
||||
matches!(
|
||||
layer.name,
|
||||
ConfigLayerSource::User { .. } | ConfigLayerSource::InMemory
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if layers.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut merged = TomlValue::Table(toml::map::Map::new());
|
||||
for layer in layers {
|
||||
merge_toml_values(&mut merged, &layer.config);
|
||||
}
|
||||
Some(merged)
|
||||
}
|
||||
|
||||
pub fn requirements(&self) -> &ConfigRequirements {
|
||||
&self.requirements
|
||||
}
|
||||
|
||||
@@ -90,6 +90,48 @@ approval_policy = "on-failure"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_user_config_includes_in_memory_but_persisted_user_config_does_not() {
|
||||
let temp_dir = TempDir::new().expect("tempdir");
|
||||
let stack = ConfigLayerStack::new(
|
||||
vec![
|
||||
ConfigLayerEntry::new(
|
||||
ConfigLayerSource::User {
|
||||
file: test_user_config_path(&temp_dir, "config.toml"),
|
||||
profile: None,
|
||||
},
|
||||
toml::toml! { model = "user" }.into(),
|
||||
),
|
||||
ConfigLayerEntry::new(
|
||||
ConfigLayerSource::InMemory,
|
||||
toml::toml! { model = "memory" }.into(),
|
||||
),
|
||||
ConfigLayerEntry::new(
|
||||
ConfigLayerSource::SessionFlags,
|
||||
toml::toml! { model = "session" }.into(),
|
||||
),
|
||||
],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("valid config layer stack");
|
||||
|
||||
assert_eq!(
|
||||
stack
|
||||
.effective_user_config()
|
||||
.and_then(|config| config.get("model").cloned())
|
||||
.and_then(|model| model.as_str().map(str::to_string)),
|
||||
Some("user".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
stack
|
||||
.effective_runtime_user_config()
|
||||
.and_then(|config| config.get("model").cloned())
|
||||
.and_then(|model| model.as_str().map(str::to_string)),
|
||||
Some("memory".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_user_config_updates_matching_user_layer_without_replacing_active_profile() {
|
||||
let temp_dir = TempDir::new().expect("tempdir");
|
||||
|
||||
@@ -18,7 +18,7 @@ pub fn installed_marketplace_roots_from_layer_stack(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
codex_home: &Path,
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
let Some(user_config) = config_layer_stack.effective_user_config() else {
|
||||
let Some(user_config) = config_layer_stack.effective_runtime_user_config() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(marketplaces_value) = user_config.get("marketplaces") else {
|
||||
|
||||
@@ -438,7 +438,7 @@ fn refresh_non_curated_plugin_cache_with_mode(
|
||||
fn configured_plugins_from_stack(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
) -> HashMap<String, PluginConfig> {
|
||||
let Some(user_config) = config_layer_stack.effective_user_config() else {
|
||||
let Some(user_config) = config_layer_stack.effective_runtime_user_config() else {
|
||||
return HashMap::new();
|
||||
};
|
||||
configured_plugins_from_user_config_value(&user_config)
|
||||
|
||||
@@ -36,6 +36,11 @@ fn configured_plugins_from_stack_merges_user_layers() {
|
||||
user_config_path(&temp_dir, "work.config.toml"),
|
||||
"[plugins.profile]\nenabled = false\n",
|
||||
),
|
||||
ConfigLayerEntry::new(
|
||||
ConfigLayerSource::InMemory,
|
||||
toml::from_str("[plugins.memory]\nenabled = true\n")
|
||||
.expect("in-memory config toml"),
|
||||
),
|
||||
],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
@@ -61,6 +66,13 @@ fn configured_plugins_from_stack_merges_user_layers() {
|
||||
mcp_servers: HashMap::new(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"memory".to_string(),
|
||||
PluginConfig {
|
||||
enabled: true,
|
||||
mcp_servers: HashMap::new(),
|
||||
},
|
||||
),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1006,7 +1006,14 @@ impl PluginsManager {
|
||||
)
|
||||
.await
|
||||
.map_err(PluginRemoteSyncError::from)?;
|
||||
let configured_plugins = configured_plugins_from_stack(&config.config_layer_stack);
|
||||
// Remote sync persists edits, so ephemeral runtime layers must not
|
||||
// influence the current persisted state.
|
||||
let configured_plugins = config
|
||||
.config_layer_stack
|
||||
.effective_user_config()
|
||||
.as_ref()
|
||||
.map(configured_plugins_from_user_config_value)
|
||||
.unwrap_or_default();
|
||||
let curated_marketplace_root = curated_plugins_repo_path(self.codex_home.as_path());
|
||||
let curated_marketplace_path = AbsolutePathBuf::try_from(
|
||||
curated_marketplace_root.join(".agents/plugins/marketplace.json"),
|
||||
@@ -2080,8 +2087,7 @@ impl PluginUninstallError {
|
||||
pub(crate) fn configured_plugins_from_stack(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
) -> HashMap<String, PluginConfig> {
|
||||
// Plugin entries remain persisted user config only.
|
||||
let Some(user_config) = config_layer_stack.effective_user_config() else {
|
||||
let Some(user_config) = config_layer_stack.effective_runtime_user_config() else {
|
||||
return HashMap::new();
|
||||
};
|
||||
configured_plugins_from_user_config_value(&user_config)
|
||||
|
||||
@@ -2886,6 +2886,27 @@ enabled = true
|
||||
.await;
|
||||
|
||||
let mut config = load_config(tmp.path(), tmp.path()).await;
|
||||
config.config_layer_stack = ConfigLayerStack::new(
|
||||
vec![
|
||||
config
|
||||
.config_layer_stack
|
||||
.get_active_user_layer()
|
||||
.unwrap()
|
||||
.clone(),
|
||||
ConfigLayerEntry::new(
|
||||
ConfigLayerSource::InMemory,
|
||||
toml::from_str(
|
||||
r#"[plugins."linear@openai-curated"]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.unwrap();
|
||||
config.chatgpt_base_url = format!("{}/backend-api/", server.uri());
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let result = manager
|
||||
|
||||
@@ -35,7 +35,9 @@ pub fn skill_config_rules_from_stack(config_layer_stack: &ConfigLayerStack) -> S
|
||||
) {
|
||||
if !matches!(
|
||||
layer.name,
|
||||
ConfigLayerSource::User { .. } | ConfigLayerSource::SessionFlags
|
||||
ConfigLayerSource::User { .. }
|
||||
| ConfigLayerSource::InMemory
|
||||
| ConfigLayerSource::SessionFlags
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -351,6 +351,7 @@ fn skill_roots_from_layer_stack_inner(
|
||||
}
|
||||
ConfigLayerSource::Mdm { .. }
|
||||
| ConfigLayerSource::EnterpriseManaged { .. }
|
||||
| ConfigLayerSource::InMemory
|
||||
| ConfigLayerSource::SessionFlags
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. }
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {}
|
||||
|
||||
@@ -1879,6 +1879,7 @@ async fn strict_config_rejects_unknown_cloud_config_key() {
|
||||
cloud_config_bundle: CloudConfigBundleFixture::loader_with_enterprise_config(
|
||||
"unknown_key = true",
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
@@ -3209,6 +3210,49 @@ async fn project_root_markers_supports_alternate_markers() -> std::io::Result<()
|
||||
.expect("foo entry");
|
||||
assert_eq!(foo, "child");
|
||||
|
||||
make_config_for_test(
|
||||
&codex_home,
|
||||
&project_root,
|
||||
TrustLevel::Trusted,
|
||||
/*project_root_markers*/ None,
|
||||
)
|
||||
.await?;
|
||||
let layers = load_config_layers_state(
|
||||
LOCAL_FS.as_ref(),
|
||||
&codex_home,
|
||||
Some(AbsolutePathBuf::from_absolute_path(&nested)?),
|
||||
&[] as &[(String, TomlValue)],
|
||||
ConfigLoadOptions {
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
in_memory_layer: Some(ConfigLayerEntry::new(
|
||||
ConfigLayerSource::InMemory,
|
||||
toml::toml! {
|
||||
project_root_markers = [".hg"]
|
||||
foo = "memory"
|
||||
}
|
||||
.into(),
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
layers
|
||||
.layers_high_to_low()
|
||||
.into_iter()
|
||||
.filter(|layer| matches!(layer.name, ConfigLayerSource::Project { .. }))
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
layers
|
||||
.effective_config()
|
||||
.get("foo")
|
||||
.and_then(TomlValue::as_str),
|
||||
Some("child")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use crate::windows_sandbox::resolve_windows_sandbox_mode;
|
||||
use crate::windows_sandbox::resolve_windows_sandbox_private_desktop;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
@@ -1107,6 +1108,7 @@ pub struct ConfigBuilder {
|
||||
loader_overrides: Option<LoaderOverrides>,
|
||||
strict_config: bool,
|
||||
cloud_config_bundle: CloudConfigBundleLoader,
|
||||
in_memory_layer: Option<ConfigLayerEntry>,
|
||||
thread_config_loader: Option<Arc<dyn ThreadConfigLoader>>,
|
||||
fallback_cwd: Option<PathBuf>,
|
||||
}
|
||||
@@ -1142,6 +1144,11 @@ impl ConfigBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn in_memory_layer(mut self, in_memory_layer: Option<ConfigLayerEntry>) -> Self {
|
||||
self.in_memory_layer = in_memory_layer;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn thread_config_loader(
|
||||
mut self,
|
||||
thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
||||
@@ -1168,6 +1175,7 @@ impl ConfigBuilder {
|
||||
loader_overrides,
|
||||
strict_config,
|
||||
cloud_config_bundle,
|
||||
in_memory_layer,
|
||||
thread_config_loader,
|
||||
fallback_cwd,
|
||||
} = self;
|
||||
@@ -1193,6 +1201,7 @@ impl ConfigBuilder {
|
||||
loader_overrides,
|
||||
strict_config,
|
||||
cloud_config_bundle,
|
||||
in_memory_layer,
|
||||
},
|
||||
thread_config_loader
|
||||
.as_deref()
|
||||
|
||||
@@ -320,6 +320,7 @@ fn is_user_controlled_layer(layer: &ConfigLayerSource) -> bool {
|
||||
matches!(
|
||||
layer,
|
||||
ConfigLayerSource::User { .. }
|
||||
| ConfigLayerSource::InMemory
|
||||
| ConfigLayerSource::Project { .. }
|
||||
| ConfigLayerSource::SessionFlags
|
||||
)
|
||||
|
||||
@@ -625,6 +625,7 @@ async fn load_config_toml_or_exit(
|
||||
loader_overrides,
|
||||
strict_config,
|
||||
cloud_config_bundle,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -9,10 +9,9 @@ use codex_config::TomlValue;
|
||||
/// Build effective hook state from config layers that are allowed to override
|
||||
/// user preferences.
|
||||
///
|
||||
/// This intentionally reads only user and session flag layers, including
|
||||
/// disabled layers, to match the skills config behavior. Project, managed, and
|
||||
/// plugin layers can discover hooks, but they do not get to write user hook
|
||||
/// state.
|
||||
/// This intentionally reads only user-controlled layers, including disabled
|
||||
/// layers, to match the skills config behavior. Project, managed, and plugin
|
||||
/// layers can discover hooks, but they do not get to write user hook state.
|
||||
pub fn hook_states_from_stack(
|
||||
config_layer_stack: Option<&ConfigLayerStack>,
|
||||
) -> HashMap<String, HookStateToml> {
|
||||
@@ -27,7 +26,9 @@ pub fn hook_states_from_stack(
|
||||
) {
|
||||
if !matches!(
|
||||
layer.name,
|
||||
ConfigLayerSource::User { .. } | ConfigLayerSource::SessionFlags
|
||||
ConfigLayerSource::User { .. }
|
||||
| ConfigLayerSource::InMemory
|
||||
| ConfigLayerSource::SessionFlags
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -374,6 +374,7 @@ fn config_toml_source_path(layer: &ConfigLayerEntry) -> AbsolutePathBuf {
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {
|
||||
synthetic_layer_path("<legacy-managed-config.toml-mdm>/managed_config.toml")
|
||||
}
|
||||
ConfigLayerSource::InMemory => synthetic_layer_path("<in-memory>/config.toml"),
|
||||
ConfigLayerSource::SessionFlags => synthetic_layer_path("<session-flags>/config.toml"),
|
||||
}
|
||||
}
|
||||
@@ -609,6 +610,7 @@ fn hook_metadata_for_config_layer_source(source: &ConfigLayerSource) -> (HookSou
|
||||
match source {
|
||||
ConfigLayerSource::System { .. } => (HookSource::System, true),
|
||||
ConfigLayerSource::User { .. } => (HookSource::User, false),
|
||||
ConfigLayerSource::InMemory => (HookSource::User, false),
|
||||
ConfigLayerSource::Project { .. } => (HookSource::Project, false),
|
||||
ConfigLayerSource::Mdm { .. } => (HookSource::Mdm, true),
|
||||
ConfigLayerSource::EnterpriseManaged { .. } => (HookSource::CloudManagedConfig, true),
|
||||
|
||||
@@ -268,7 +268,9 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec<Line<'static>> {
|
||||
|
||||
fn render_non_file_layer_details(layer: &ConfigLayerEntry) -> Vec<Line<'static>> {
|
||||
match &layer.name {
|
||||
ConfigLayerSource::SessionFlags => render_session_flag_details(&layer.config),
|
||||
ConfigLayerSource::InMemory | ConfigLayerSource::SessionFlags => {
|
||||
render_session_flag_details(&layer.config)
|
||||
}
|
||||
ConfigLayerSource::Mdm { .. }
|
||||
| ConfigLayerSource::EnterpriseManaged { .. }
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => render_non_file_layer_value(layer),
|
||||
@@ -335,7 +337,8 @@ fn non_file_layer_value_label(source: &ConfigLayerSource) -> &'static str {
|
||||
"MDM value"
|
||||
}
|
||||
ConfigLayerSource::EnterpriseManaged { .. } => "Enterprise-managed config value",
|
||||
ConfigLayerSource::SessionFlags
|
||||
ConfigLayerSource::InMemory
|
||||
| ConfigLayerSource::SessionFlags
|
||||
| ConfigLayerSource::System { .. }
|
||||
| ConfigLayerSource::User { .. }
|
||||
| ConfigLayerSource::Project { .. }
|
||||
|
||||
@@ -1968,6 +1968,7 @@ async fn load_config_toml_or_exit(
|
||||
loader_overrides,
|
||||
strict_config,
|
||||
cloud_config_bundle,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -253,6 +253,7 @@ async fn start_app_server_for_archive_command(
|
||||
loader_overrides: loader_overrides.clone(),
|
||||
strict_config,
|
||||
cloud_config_bundle: CloudConfigBundleLoader::default(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user