From 9a5113ad7306ab7440a5fa8e20ce6e6af2c4efed Mon Sep 17 00:00:00 2001 From: Aaron Levine Date: Wed, 3 Jun 2026 20:33:06 -0700 Subject: [PATCH] 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 --- .../schema/json/ClientRequest.json | 37 ++++++++ .../PermissionsRequestApprovalParams.json | 6 +- .../schema/json/ServerRequest.json | 6 +- .../codex_app_server_protocol.schemas.json | 67 +++++++++++++- .../codex_app_server_protocol.v2.schemas.json | 61 +++++++++++++ .../json/v2/ConfigInMemorySetParams.json | 15 ++++ .../json/v2/ConfigInMemorySetResponse.json | 5 ++ .../schema/json/v2/ConfigReadResponse.json | 17 ++++ .../schema/json/v2/ConfigWriteResponse.json | 17 ++++ .../schema/typescript/ClientRequest.ts | 3 +- .../typescript/v2/ConfigInMemorySetParams.ts | 11 +++ .../v2/ConfigInMemorySetResponse.ts | 5 ++ .../schema/typescript/v2/ConfigLayerSource.ts | 2 +- .../schema/typescript/v2/index.ts | 2 + .../src/protocol/common.rs | 5 ++ .../src/protocol/v2/config.rs | 18 ++++ codex-rs/app-server/README.md | 3 +- codex-rs/app-server/src/config_manager.rs | 35 +++++++- .../app-server/src/config_manager_service.rs | 76 ++++++++++++++++ .../src/config_manager_service_tests.rs | 73 ++++++++++++++++ codex-rs/app-server/src/message_processor.rs | 5 ++ .../request_processors/config_processor.rs | 15 ++++ .../src/request_processors/turn_processor.rs | 1 + codex-rs/config/src/config_layer_source.rs | 1 + codex-rs/config/src/diagnostics.rs | 1 + codex-rs/config/src/loader/README.md | 9 +- codex-rs/config/src/loader/mod.rs | 87 ++++++++++++++++--- codex-rs/config/src/state.rs | 32 +++++++ codex-rs/config/src/state_tests.rs | 42 +++++++++ .../src/installed_marketplaces.rs | 2 +- codex-rs/core-plugins/src/loader.rs | 2 +- codex-rs/core-plugins/src/loader_tests.rs | 12 +++ codex-rs/core-plugins/src/manager.rs | 12 ++- codex-rs/core-plugins/src/manager_tests.rs | 21 +++++ codex-rs/core-skills/src/config_rules.rs | 4 +- codex-rs/core-skills/src/loader.rs | 1 + .../core/src/config/config_loader_tests.rs | 44 ++++++++++ codex-rs/core/src/config/mod.rs | 9 ++ codex-rs/core/src/network_proxy_loader.rs | 1 + codex-rs/exec/src/lib.rs | 1 + codex-rs/hooks/src/config_rules.rs | 11 +-- codex-rs/hooks/src/engine/discovery.rs | 2 + codex-rs/tui/src/debug_config.rs | 7 +- codex-rs/tui/src/lib.rs | 1 + codex-rs/tui/src/session_archive_commands.rs | 1 + 45 files changed, 747 insertions(+), 41 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/json/v2/ConfigInMemorySetParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/ConfigInMemorySetResponse.json create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ConfigInMemorySetParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ConfigInMemorySetResponse.ts diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index dc5dd6ad60..bb18031def 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -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": { diff --git a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json index 961e24c2c2..f2ab783342 100644 --- a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json @@ -288,8 +288,8 @@ "environmentId": { "default": null, "type": [ - "null", - "string" + "string", + "null" ] }, "itemId": { @@ -326,4 +326,4 @@ ], "title": "PermissionsRequestApprovalParams", "type": "object" -} +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index 13bac2e118..dbfca64f4c 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -1593,8 +1593,8 @@ "environmentId": { "default": null, "type": [ - "null", - "string" + "string", + "null" ] }, "itemId": { @@ -2005,4 +2005,4 @@ } ], "title": "ServerRequest" -} +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 6aa4c5f35e..205fa8a2e4 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -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" -} +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 6d859ee5f3..9ca68b50f8 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -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": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigInMemorySetParams.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigInMemorySetParams.json new file mode 100644 index 0000000000..9817268ace --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigInMemorySetParams.json @@ -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" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigInMemorySetResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigInMemorySetResponse.json new file mode 100644 index 0000000000..9143e82bfc --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigInMemorySetResponse.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigInMemorySetResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json index 09cf897898..e8f14a6a58 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json @@ -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": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigWriteResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigWriteResponse.json index d1a2254a92..36512d4a43 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigWriteResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigWriteResponse.json @@ -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": { diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index e19ac2dfde..cf9b6907de 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -14,6 +14,7 @@ import type { CommandExecResizeParams } from "./v2/CommandExecResizeParams"; import type { CommandExecTerminateParams } from "./v2/CommandExecTerminateParams"; import type { CommandExecWriteParams } from "./v2/CommandExecWriteParams"; import type { ConfigBatchWriteParams } from "./v2/ConfigBatchWriteParams"; +import type { ConfigInMemorySetParams } from "./v2/ConfigInMemorySetParams"; import type { ConfigReadParams } from "./v2/ConfigReadParams"; import type { ConfigValueWriteParams } from "./v2/ConfigValueWriteParams"; import type { ExperimentalFeatureEnablementSetParams } from "./v2/ExperimentalFeatureEnablementSetParams"; @@ -86,4 +87,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "config/inMemory/set", id: RequestId, params: ConfigInMemorySetParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigInMemorySetParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigInMemorySetParams.ts new file mode 100644 index 0000000000..f8a5ac5516 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigInMemorySetParams.ts @@ -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 }, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigInMemorySetResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigInMemorySetResponse.ts new file mode 100644 index 0000000000..a3088de6a0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigInMemorySetResponse.ts @@ -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; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigLayerSource.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigLayerSource.ts index 963c331b5d..d0f2c1f909 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigLayerSource.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigLayerSource.ts @@ -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" }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 48e1f9580b..cf4b992dc5 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -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"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 5a60d8d002..bff10e742c 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -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"), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index f40192ddc6..5903f7c14f 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -72,6 +72,9 @@ pub enum ConfigLayerSource { profile: Option, }, + /// 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>, } +#[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, +} + +#[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/")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index dc02ff8c04..f173768e29 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -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. diff --git a/codex-rs/app-server/src/config_manager.rs b/codex-rs/app-server/src/config_manager.rs index fae113154b..b08d1a80f8 100644 --- a/codex-rs/app-server/src/config_manager.rs +++ b/codex-rs/app-server/src/config_manager.rs @@ -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>>, runtime_feature_enablement: Arc>>, + in_memory_layer: Arc>>, loader_overrides: LoaderOverrides, - strict_config: bool, + pub(super) strict_config: bool, cloud_config_bundle: Arc>, arg0_paths: Arg0DispatchPaths, thread_config_loader: Arc>>, @@ -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> { + 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(&self, enablement: I) -> Result<(), ()> where I: IntoIterator, @@ -115,6 +125,18 @@ impl ConfigManager { } } + pub(crate) fn replace_in_memory_layer( + &self, + in_memory_layer: Option, + ) -> 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 { 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, + ) -> std::io::Result { + 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, + in_memory_layer: Option, ) -> std::io::Result { 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(), ) diff --git a/codex-rs/app-server/src/config_manager_service.rs b/codex-rs/app-server/src/config_manager_service.rs index 4b42c28aa9..ea9ba845d3 100644 --- a/codex-rs/app-server/src/config_manager_service.rs +++ b/codex-rs/app-server/src/config_manager_service.rs @@ -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 { + 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, @@ -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(), diff --git a/codex-rs/app-server/src/config_manager_service_tests.rs b/codex-rs/app-server/src/config_manager_service_tests.rs index 8deae8b065..a9bca85e7f 100644 --- a/codex-rs/app-server/src/config_manager_service_tests.rs +++ b/codex-rs/app-server/src/config_manager_service_tests.rs @@ -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"); diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 84f22809ee..52646b5249 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -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() diff --git a/codex-rs/app-server/src/request_processors/config_processor.rs b/codex-rs/app-server/src/request_processors/config_processor.rs index 3571026193..39c1b264fc 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -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 { + 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, diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index 25beee3cdb..b9ef952dcd 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -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()); diff --git a/codex-rs/config/src/config_layer_source.rs b/codex-rs/config/src/config_layer_source.rs index 7bff779b19..0d55f82182 100644 --- a/codex-rs/config/src/config_layer_source.rs +++ b/codex-rs/config/src/config_layer_source.rs @@ -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})", diff --git a/codex-rs/config/src/diagnostics.rs b/codex-rs/config/src/diagnostics.rs index ca7df712d6..a39003cfef 100644 --- a/codex-rs/config/src/diagnostics.rs +++ b/codex-rs/config/src/diagnostics.rs @@ -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, } diff --git a/codex-rs/config/src/loader/README.md b/codex-rs/config/src/loader/README.md index b711ebbb61..3b1580433c 100644 --- a/codex-rs/config/src/loader/README.md +++ b/codex-rs/config/src/loader/README.md @@ -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 diff --git a/codex-rs/config/src/loader/mod.rs b/codex-rs/config/src/loader/mod.rs index bc0fb482e4..68440fb742 100644 --- a/codex-rs/config/src/loader/mod.rs +++ b/codex-rs/config/src/loader/mod.rs @@ -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}/.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::(cli_overrides_layer.clone()) - { + if let Some(ignored_path) = ignored_toml_value_field::(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 { + 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 { + 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 { diff --git a/codex-rs/config/src/state.rs b/codex-rs/config/src/state.rs index d5f2b050c9..02ff122535 100644 --- a/codex-rs/config/src/state.rs +++ b/codex-rs/config/src/state.rs @@ -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, } impl From for ConfigLoadOptions { @@ -31,6 +32,7 @@ impl From 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 { + let layers = self + .get_layers( + ConfigLayerStackOrdering::LowestPrecedenceFirst, + /*include_disabled*/ false, + ) + .into_iter() + .filter(|layer| { + matches!( + layer.name, + ConfigLayerSource::User { .. } | ConfigLayerSource::InMemory + ) + }) + .collect::>(); + 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 } diff --git a/codex-rs/config/src/state_tests.rs b/codex-rs/config/src/state_tests.rs index fb6e26968b..6984e9ca96 100644 --- a/codex-rs/config/src/state_tests.rs +++ b/codex-rs/config/src/state_tests.rs @@ -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"); diff --git a/codex-rs/core-plugins/src/installed_marketplaces.rs b/codex-rs/core-plugins/src/installed_marketplaces.rs index 0377fac9ed..f17a0cb398 100644 --- a/codex-rs/core-plugins/src/installed_marketplaces.rs +++ b/codex-rs/core-plugins/src/installed_marketplaces.rs @@ -18,7 +18,7 @@ pub fn installed_marketplace_roots_from_layer_stack( config_layer_stack: &ConfigLayerStack, codex_home: &Path, ) -> Vec { - 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 { diff --git a/codex-rs/core-plugins/src/loader.rs b/codex-rs/core-plugins/src/loader.rs index d51771085d..47d97cdf15 100644 --- a/codex-rs/core-plugins/src/loader.rs +++ b/codex-rs/core-plugins/src/loader.rs @@ -438,7 +438,7 @@ fn refresh_non_curated_plugin_cache_with_mode( fn configured_plugins_from_stack( config_layer_stack: &ConfigLayerStack, ) -> HashMap { - 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) diff --git a/codex-rs/core-plugins/src/loader_tests.rs b/codex-rs/core-plugins/src/loader_tests.rs index a360d54ccb..b51ed276f9 100644 --- a/codex-rs/core-plugins/src/loader_tests.rs +++ b/codex-rs/core-plugins/src/loader_tests.rs @@ -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(), + }, + ), ]) ); } diff --git a/codex-rs/core-plugins/src/manager.rs b/codex-rs/core-plugins/src/manager.rs index 8022c6c86d..3f39eaa20f 100644 --- a/codex-rs/core-plugins/src/manager.rs +++ b/codex-rs/core-plugins/src/manager.rs @@ -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 { - // 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) diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index 905d120a8e..208c58f8d3 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -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 diff --git a/codex-rs/core-skills/src/config_rules.rs b/codex-rs/core-skills/src/config_rules.rs index 92ad2ab1a6..86f84d2f12 100644 --- a/codex-rs/core-skills/src/config_rules.rs +++ b/codex-rs/core-skills/src/config_rules.rs @@ -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; } diff --git a/codex-rs/core-skills/src/loader.rs b/codex-rs/core-skills/src/loader.rs index 7dac256010..b00bbf0498 100644 --- a/codex-rs/core-skills/src/loader.rs +++ b/codex-rs/core-skills/src/loader.rs @@ -351,6 +351,7 @@ fn skill_roots_from_layer_stack_inner( } ConfigLayerSource::Mdm { .. } | ConfigLayerSource::EnterpriseManaged { .. } + | ConfigLayerSource::InMemory | ConfigLayerSource::SessionFlags | ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } | ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {} diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 5edecebec0..1570b284e4 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -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(()) } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 5b79672b81..6d8f5d764a 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -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, strict_config: bool, cloud_config_bundle: CloudConfigBundleLoader, + in_memory_layer: Option, thread_config_loader: Option>, fallback_cwd: Option, } @@ -1142,6 +1144,11 @@ impl ConfigBuilder { self } + pub fn in_memory_layer(mut self, in_memory_layer: Option) -> Self { + self.in_memory_layer = in_memory_layer; + self + } + pub fn thread_config_loader( mut self, thread_config_loader: Arc, @@ -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() diff --git a/codex-rs/core/src/network_proxy_loader.rs b/codex-rs/core/src/network_proxy_loader.rs index b91eaa26ca..f647a4eec6 100644 --- a/codex-rs/core/src/network_proxy_loader.rs +++ b/codex-rs/core/src/network_proxy_loader.rs @@ -320,6 +320,7 @@ fn is_user_controlled_layer(layer: &ConfigLayerSource) -> bool { matches!( layer, ConfigLayerSource::User { .. } + | ConfigLayerSource::InMemory | ConfigLayerSource::Project { .. } | ConfigLayerSource::SessionFlags ) diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 406197d720..6a5a94d631 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -625,6 +625,7 @@ async fn load_config_toml_or_exit( loader_overrides, strict_config, cloud_config_bundle, + ..Default::default() }, ) .await diff --git a/codex-rs/hooks/src/config_rules.rs b/codex-rs/hooks/src/config_rules.rs index 2935cabc80..718078ee49 100644 --- a/codex-rs/hooks/src/config_rules.rs +++ b/codex-rs/hooks/src/config_rules.rs @@ -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 { @@ -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; } diff --git a/codex-rs/hooks/src/engine/discovery.rs b/codex-rs/hooks/src/engine/discovery.rs index 92d17fbabd..f2664fe36b 100644 --- a/codex-rs/hooks/src/engine/discovery.rs +++ b/codex-rs/hooks/src/engine/discovery.rs @@ -374,6 +374,7 @@ fn config_toml_source_path(layer: &ConfigLayerEntry) -> AbsolutePathBuf { ConfigLayerSource::LegacyManagedConfigTomlFromMdm => { synthetic_layer_path("/managed_config.toml") } + ConfigLayerSource::InMemory => synthetic_layer_path("/config.toml"), ConfigLayerSource::SessionFlags => synthetic_layer_path("/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), diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 562df95989..28ef2914a0 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -268,7 +268,9 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec> { fn render_non_file_layer_details(layer: &ConfigLayerEntry) -> Vec> { 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 { .. } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index b53ec0f83d..cf537d6cfb 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -1968,6 +1968,7 @@ async fn load_config_toml_or_exit( loader_overrides, strict_config, cloud_config_bundle, + ..Default::default() }, ) .await diff --git a/codex-rs/tui/src/session_archive_commands.rs b/codex-rs/tui/src/session_archive_commands.rs index 4e5c420f63..718e1aff1d 100644 --- a/codex-rs/tui/src/session_archive_commands.rs +++ b/codex-rs/tui/src/session_archive_commands.rs @@ -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