diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 97b5f698c27c..ef3e54a5643b 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -413,6 +413,19 @@ ], "type": "object" }, + "ConfigHostSetParams": { + "properties": { + "config": { + "additionalProperties": true, + "description": "Full replacement app-server host config layer, shaped like config.toml. An empty object clears the host config layer.", + "type": "object" + } + }, + "required": [ + "config" + ], + "type": "object" + }, "ConfigReadParams": { "properties": { "cwd": { @@ -5981,6 +5994,30 @@ "title": "Config/readRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "config/host/set" + ], + "title": "Config/host/setRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ConfigHostSetParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Config/host/setRequest", + "type": "object" + }, { "properties": { "id": { 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 6c2eb114355c..fdde625b7c9d 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 @@ -1981,6 +1981,30 @@ "title": "Config/readRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "config/host/set" + ], + "title": "Config/host/setRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ConfigHostSetParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Config/host/setRequest", + "type": "object" + }, { "properties": { "id": { @@ -7479,6 +7503,26 @@ ], "type": "object" }, + "ConfigHostSetParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "config": { + "additionalProperties": true, + "description": "Full replacement app-server host config layer, shaped like config.toml. An empty object clears the host config layer.", + "type": "object" + } + }, + "required": [ + "config" + ], + "title": "ConfigHostSetParams", + "type": "object" + }, + "ConfigHostSetResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigHostSetResponse", + "type": "object" + }, "ConfigLayer": { "properties": { "config": true, @@ -7603,6 +7647,23 @@ "title": "UserConfigLayerSource", "type": "object" }, + { + "description": "In-memory host-supplied config for the current app-server instance.", + "properties": { + "type": { + "enum": [ + "appServerHost" + ], + "title": "AppServerHostConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "AppServerHostConfigLayerSource", + "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/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 1b0647179e88..b6826b82c72d 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 @@ -2707,6 +2707,30 @@ "title": "Config/readRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "config/host/set" + ], + "title": "Config/host/setRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ConfigHostSetParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Config/host/setRequest", + "type": "object" + }, { "properties": { "id": { @@ -3848,6 +3872,26 @@ ], "type": "object" }, + "ConfigHostSetParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "config": { + "additionalProperties": true, + "description": "Full replacement app-server host config layer, shaped like config.toml. An empty object clears the host config layer.", + "type": "object" + } + }, + "required": [ + "config" + ], + "title": "ConfigHostSetParams", + "type": "object" + }, + "ConfigHostSetResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigHostSetResponse", + "type": "object" + }, "ConfigLayer": { "properties": { "config": true, @@ -3972,6 +4016,23 @@ "title": "UserConfigLayerSource", "type": "object" }, + { + "description": "In-memory host-supplied config for the current app-server instance.", + "properties": { + "type": { + "enum": [ + "appServerHost" + ], + "title": "AppServerHostConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "AppServerHostConfigLayerSource", + "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/ConfigHostSetParams.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigHostSetParams.json new file mode 100644 index 000000000000..901634be9c89 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigHostSetParams.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "config": { + "additionalProperties": true, + "description": "Full replacement app-server host config layer, shaped like config.toml. An empty object clears the host config layer.", + "type": "object" + } + }, + "required": [ + "config" + ], + "title": "ConfigHostSetParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigHostSetResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigHostSetResponse.json new file mode 100644 index 000000000000..fd4da0fa8963 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigHostSetResponse.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigHostSetResponse", + "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 7595f7fd0093..95fe898f57ec 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json @@ -544,6 +544,23 @@ "title": "UserConfigLayerSource", "type": "object" }, + { + "description": "In-memory host-supplied config for the current app-server instance.", + "properties": { + "type": { + "enum": [ + "appServerHost" + ], + "title": "AppServerHostConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "AppServerHostConfigLayerSource", + "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 37816463b607..e1f70c75bc30 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigWriteResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigWriteResponse.json @@ -106,6 +106,23 @@ "title": "UserConfigLayerSource", "type": "object" }, + { + "description": "In-memory host-supplied config for the current app-server instance.", + "properties": { + "type": { + "enum": [ + "appServerHost" + ], + "title": "AppServerHostConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "AppServerHostConfigLayerSource", + "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 86ba2cd171fa..3c9a1efe4dd4 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 { ConfigHostSetParams } from "./v2/ConfigHostSetParams"; import type { ConfigReadParams } from "./v2/ConfigReadParams"; import type { ConfigValueWriteParams } from "./v2/ConfigValueWriteParams"; import type { ExperimentalFeatureEnablementSetParams } from "./v2/ExperimentalFeatureEnablementSetParams"; @@ -85,4 +86,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": "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": "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/host/set", id: RequestId, params: ConfigHostSetParams, } | { "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/ConfigHostSetParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigHostSetParams.ts new file mode 100644 index 000000000000..ba36d478ccd5 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigHostSetParams.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 ConfigHostSetParams = { +/** + * Full replacement app-server host config layer, shaped like config.toml. + * An empty object clears the host config layer. + */ +config: { [key in string]?: JsonValue }, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigHostSetResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigHostSetResponse.ts new file mode 100644 index 000000000000..63bf32c3bb54 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigHostSetResponse.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 ConfigHostSetResponse = 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 08cb8c6bfd8b..8c73adad1f1b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigLayerSource.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigLayerSource.ts @@ -18,4 +18,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": "appServerHost" } | { "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 e9b8993c26e0..a6704ae00f38 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -66,6 +66,8 @@ export type { ComputerUseRequirements } from "./ComputerUseRequirements"; export type { Config } from "./Config"; export type { ConfigBatchWriteParams } from "./ConfigBatchWriteParams"; export type { ConfigEdit } from "./ConfigEdit"; +export type { ConfigHostSetParams } from "./ConfigHostSetParams"; +export type { ConfigHostSetResponse } from "./ConfigHostSetResponse"; 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 76334e8a6af0..18ac538e15e8 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -991,6 +991,11 @@ client_request_definitions! { serialization: global_shared_read("config"), response: v2::ConfigReadResponse, }, + ConfigHostSet => "config/host/set" { + params: v2::ConfigHostSetParams, + serialization: global("config"), + response: v2::ConfigHostSetResponse, + }, 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 98466078e220..e508dc5ae026 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -58,6 +58,9 @@ pub enum ConfigLayerSource { profile: Option, }, + /// In-memory host-supplied config for the current app-server instance. + AppServerHost, + /// 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")] @@ -96,6 +99,7 @@ impl ConfigLayerSource { 20 } } + ConfigLayerSource::AppServerHost => 22, ConfigLayerSource::Project { .. } => 25, ConfigLayerSource::SessionFlags => 30, ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => 40, @@ -377,6 +381,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 ConfigHostSetParams { + /// Full replacement app-server host config layer, shaped like config.toml. + /// An empty object clears the host config layer. + pub config: HashMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ConfigHostSetResponse {} + #[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 8ce55410a1a8..865deba410e7 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -221,13 +221,25 @@ 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/host/set` — replace the in-memory host config layer for this app-server process. The layer is shaped like `config.toml`, sits above user config and below project/session overrides, and `{ "config": {} }` clears it. - `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. - `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits. - `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), lifecycle hook lockdown (`allowManagedHooksOnly`), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. +### Example: Set host config for one app-server process + +Use `config/host/set` when the host needs an app-server-scoped config layer that behaves like an in-memory `config.toml` layer for later config reads and thread creation. + +```json +{ "method": "config/host/set", "id": 7, "params": { "config": { "model": "gpt-5.4" } } } +{ "id": 7, "result": {} } +{ "method": "config/host/set", "id": 8, "params": { "config": {} } } +{ "id": 8, "result": {} } +``` + ### Example: Start or resume a thread Start a fresh thread when you need a new Codex conversation. diff --git a/codex-rs/app-server/src/config/external_agent_config.rs b/codex-rs/app-server/src/config/external_agent_config.rs index 18c276f07d72..39708f5d3893 100644 --- a/codex-rs/app-server/src/config/external_agent_config.rs +++ b/codex-rs/app-server/src/config/external_agent_config.rs @@ -1,3 +1,5 @@ +use codex_config::ConfigLayerSource; +use codex_config::ConfigLayerStackOrdering; use codex_config::types::PluginConfig; use codex_core::config::Config; use codex_core::config::ConfigBuilder; @@ -124,13 +126,22 @@ impl ExternalAgentConfigService { } } + #[cfg(test)] pub(crate) async fn detect( &self, params: ExternalAgentConfigDetectOptions, + ) -> io::Result> { + self.detect_with_config(params, /*config*/ None).await + } + + pub(crate) async fn detect_with_config( + &self, + params: ExternalAgentConfigDetectOptions, + config: Option<&Config>, ) -> io::Result> { let mut items = Vec::new(); if params.include_home { - self.detect_migrations(/*repo_root*/ None, &mut items) + self.detect_migrations(/*repo_root*/ None, config, &mut items) .await?; } @@ -138,7 +149,8 @@ impl ExternalAgentConfigService { let Some(repo_root) = find_repo_root(Some(cwd))? else { continue; }; - self.detect_migrations(Some(&repo_root), &mut items).await?; + self.detect_migrations(Some(&repo_root), config, &mut items) + .await?; } Ok(items) @@ -261,6 +273,7 @@ impl ExternalAgentConfigService { async fn detect_migrations( &self, repo_root: Option<&Path>, + app_server_config: Option<&Config>, items: &mut Vec, ) -> io::Result<()> { let cwd = repo_root.map(Path::to_path_buf); @@ -498,49 +511,66 @@ impl ExternalAgentConfigService { } if let Some(settings) = settings.as_ref() { - match ConfigBuilder::default() - .codex_home(self.codex_home.clone()) - .fallback_cwd(Some(self.codex_home.clone())) - .build() - .await - { - Ok(config) => { - let configured_plugin_ids = config - .config_layer_stack - .get_active_user_layer() - .and_then(|user_layer| user_layer.config.get("plugins")) - .and_then(|plugins| { - match plugins.clone().try_into::>() { - Ok(plugins) => Some(plugins), - Err(err) => { - tracing::warn!("invalid plugins config: {err}"); - None - } - } - }) - .map(|plugins| plugins.into_keys().collect::>()) - .unwrap_or_default(); - let configured_marketplace_plugins = configured_marketplace_plugins( - &config, - &PluginsManager::new(self.codex_home.clone()), - )?; - if let Some(item) = self.detect_plugin_migration( - source_settings.as_path(), - repo_root.unwrap_or(self.external_agent_home.as_path()), - cwd.clone(), - settings, - &configured_plugin_ids, - &configured_marketplace_plugins, - ) { - items.push(item); + let fallback_config = if app_server_config.is_none() { + match ConfigBuilder::default() + .codex_home(self.codex_home.clone()) + .fallback_cwd(Some(self.codex_home.clone())) + .build() + .await + { + Ok(config) => Some(config), + Err(err) => { + tracing::warn!( + error = %err, + settings_path = %source_settings.display(), + "skipping external agent plugin migration detection because config load failed" + ); + None } } - Err(err) => { - tracing::warn!( - error = %err, - settings_path = %source_settings.display(), - "skipping external agent plugin migration detection because config load failed" - ); + } else { + None + }; + + if let Some(config) = app_server_config.or(fallback_config.as_ref()) { + let configured_plugin_ids = config + .config_layer_stack + .get_layers( + ConfigLayerStackOrdering::LowestPrecedenceFirst, + /*include_disabled*/ false, + ) + .into_iter() + .filter(|layer| { + matches!( + layer.name, + ConfigLayerSource::User { .. } | ConfigLayerSource::AppServerHost + ) + }) + .filter_map(|layer| layer.config.get("plugins")) + .filter_map(|plugins| { + match plugins.clone().try_into::>() { + Ok(plugins) => Some(plugins), + Err(err) => { + tracing::warn!("invalid plugins config: {err}"); + None + } + } + }) + .flat_map(HashMap::into_keys) + .collect::>(); + let configured_marketplace_plugins = configured_marketplace_plugins( + config, + &PluginsManager::new(self.codex_home.clone()), + )?; + if let Some(item) = self.detect_plugin_migration( + source_settings.as_path(), + repo_root.unwrap_or(self.external_agent_home.as_path()), + cwd.clone(), + settings, + &configured_plugin_ids, + &configured_marketplace_plugins, + ) { + items.push(item); } } } diff --git a/codex-rs/app-server/src/config/external_agent_config_tests.rs b/codex-rs/app-server/src/config/external_agent_config_tests.rs index 07b09c5a1e43..1ace2526f707 100644 --- a/codex-rs/app-server/src/config/external_agent_config_tests.rs +++ b/codex-rs/app-server/src/config/external_agent_config_tests.rs @@ -1531,6 +1531,81 @@ enabled = true ); } +#[tokio::test] +async fn detect_repo_skips_plugins_that_are_already_configured_by_app_server_host() { + let root = TempDir::new().expect("create tempdir"); + let external_agent_home = root.path().join(EXTERNAL_AGENT_DIR); + let codex_home = root.path().join(".codex"); + let repo_root = root.path().join("repo"); + fs::create_dir_all(repo_root.join(".git")).expect("create git dir"); + fs::create_dir_all(repo_root.join(EXTERNAL_AGENT_DIR)).expect("create repo external agent dir"); + fs::create_dir_all(&codex_home).expect("create codex home"); + fs::write( + repo_root.join(EXTERNAL_AGENT_DIR).join("settings.json"), + r#"{ + "enabledPlugins": { + "formatter@acme-tools": true, + "deployer@acme-tools": true + }, + "extraKnownMarketplaces": { + "acme-tools": { + "source": "acme-corp/external-agent-plugins" + } + } + }"#, + ) + .expect("write repo settings"); + + let config = ConfigBuilder::default() + .codex_home(codex_home.clone()) + .fallback_cwd(Some(repo_root.clone())) + .app_server_host_config(Some( + toml::from_str( + r#" +[plugins."formatter@acme-tools"] +enabled = true +"#, + ) + .expect("host config toml"), + )) + .build() + .await + .expect("build config"); + + let items = service_for_paths(external_agent_home, codex_home) + .detect_with_config( + ExternalAgentConfigDetectOptions { + include_home: false, + cwds: Some(vec![repo_root.clone()]), + }, + Some(&config), + ) + .await + .expect("detect"); + + assert_eq!( + items, + vec![ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Plugins, + description: format!( + "Migrate enabled plugins from {}", + repo_root + .join(EXTERNAL_AGENT_DIR) + .join("settings.json") + .display() + ), + cwd: Some(repo_root), + details: Some(MigrationDetails { + plugins: vec![PluginsMigration { + marketplace_name: "acme-tools".to_string(), + plugin_names: vec!["deployer".to_string()], + }], + ..Default::default() + }), + }] + ); +} + #[tokio::test] async fn detect_repo_skips_plugins_that_are_disabled_in_codex() { let root = TempDir::new().expect("create tempdir"); diff --git a/codex-rs/app-server/src/config_manager.rs b/codex-rs/app-server/src/config_manager.rs index 25fdc5c0cb84..0659e8d36d2b 100644 --- a/codex-rs/app-server/src/config_manager.rs +++ b/codex-rs/app-server/src/config_manager.rs @@ -29,6 +29,7 @@ pub(crate) struct ConfigManager { codex_home: PathBuf, cli_overrides: Arc>>, runtime_feature_enablement: Arc>>, + app_server_host_config: Arc>>, loader_overrides: LoaderOverrides, strict_config: bool, cloud_requirements: Arc>, @@ -50,6 +51,7 @@ impl ConfigManager { codex_home, cli_overrides: Arc::new(RwLock::new(cli_overrides)), runtime_feature_enablement: Arc::new(RwLock::new(BTreeMap::new())), + app_server_host_config: Arc::new(RwLock::new(None)), loader_overrides, strict_config, cloud_requirements: Arc::new(RwLock::new(cloud_requirements)), @@ -80,6 +82,13 @@ impl ConfigManager { .unwrap_or_default() } + pub(crate) fn current_app_server_host_config(&self) -> Option { + self.app_server_host_config + .read() + .map(|guard| guard.clone()) + .unwrap_or_default() + } + pub(crate) fn extend_runtime_feature_enablement(&self, enablement: I) -> Result<(), ()> where I: IntoIterator, @@ -115,6 +124,15 @@ impl ConfigManager { } } + pub(crate) fn replace_app_server_host_config( + &self, + app_server_host_config: Option, + ) -> Result<(), ()> { + let mut guard = self.app_server_host_config.write().map_err(|_| ())?; + *guard = app_server_host_config; + Ok(()) + } + fn current_thread_config_loader(&self) -> Arc { self.thread_config_loader .read() @@ -235,6 +253,7 @@ impl ConfigManager { .cli_overrides(merged_cli_overrides) .loader_overrides(self.loader_overrides.clone()) .strict_config(self.strict_config) + .app_server_host_config(self.current_app_server_host_config()) .harness_overrides(typesafe_overrides) .fallback_cwd(fallback_cwd) .cloud_requirements(self.current_cloud_requirements()) @@ -256,6 +275,18 @@ impl ConfigManager { pub(crate) async fn load_config_layers( &self, cwd: Option, + ) -> std::io::Result { + self.load_config_layers_with_app_server_host_config( + cwd, + self.current_app_server_host_config(), + ) + .await + } + + pub(crate) async fn load_config_layers_with_app_server_host_config( + &self, + cwd: Option, + app_server_host_config: Option, ) -> std::io::Result { let thread_config_loader = self.current_thread_config_loader(); load_config_layers_state( @@ -266,6 +297,7 @@ impl ConfigManager { codex_config::ConfigLoadOptions { loader_overrides: self.loader_overrides.clone(), strict_config: self.strict_config, + app_server_host_config, }, self.current_cloud_requirements(), 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 4255b83e62a3..f2deeaa00e72 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::ConfigHostSetParams; +use codex_app_server_protocol::ConfigHostSetResponse; use codex_app_server_protocol::ConfigLayerMetadata; use codex_app_server_protocol::ConfigLayerSource; use codex_app_server_protocol::ConfigReadParams; @@ -29,6 +31,7 @@ use codex_core::path_utils::write_atomically; use codex_utils_absolute_path::AbsolutePathBuf; use serde_json::Value as JsonValue; use std::borrow::Cow; +use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; use thiserror::Error; @@ -190,6 +193,65 @@ impl ConfigManager { .await } + pub(crate) async fn set_host_config( + &self, + params: ConfigHostSetParams, + ) -> Result { + let app_server_host_config = parse_app_server_host_config(params.config)?; + + let layers = self + .load_config_layers_with_app_server_host_config( + /*cwd*/ None, + app_server_host_config.clone(), + ) + .await + .map_err(|err| { + ConfigManagerError::write( + ConfigWriteErrorCode::ConfigValidationError, + format!("Invalid app-server host configuration: {err}"), + ) + })?; + + if let Some(host_config) = app_server_host_config.as_ref() { + let host_config_toml = + deserialize_config_toml_with_base(host_config.clone(), self.codex_home()).map_err( + |err| { + ConfigManagerError::write( + ConfigWriteErrorCode::ConfigValidationError, + format!("Invalid app-server host configuration: {err}"), + ) + }, + )?; + validate_feature_requirements_for_config_toml( + &host_config_toml, + layers.requirements().feature_requirements.as_ref(), + ) + .map_err(|err| { + ConfigManagerError::write( + ConfigWriteErrorCode::ConfigValidationError, + format!("Invalid app-server host configuration: {err}"), + ) + })?; + } + + validate_config(&layers.effective_config()).map_err(|err| { + ConfigManagerError::write( + ConfigWriteErrorCode::ConfigValidationError, + format!("Invalid app-server host configuration: {err}"), + ) + })?; + + self.replace_app_server_host_config(app_server_host_config) + .map_err(|()| { + ConfigManagerError::io( + "failed to update app-server host config", + std::io::Error::other("app-server host config lock poisoned"), + ) + })?; + + Ok(ConfigHostSetResponse {}) + } + async fn apply_edits( &self, file_path: Option, @@ -399,6 +461,25 @@ fn parse_value(value: JsonValue) -> Result, String> { .map_err(|err| format!("invalid value: {err}")) } +fn parse_app_server_host_config( + config: HashMap, +) -> Result, ConfigManagerError> { + if config.is_empty() { + return Ok(None); + } + + let host_config = serde_json::to_value(config) + .and_then(serde_json::from_value::) + .map_err(|err| { + ConfigManagerError::write( + ConfigWriteErrorCode::ConfigValidationError, + format!("invalid app-server host config: {err}"), + ) + })?; + + Ok(Some(host_config)) +} + fn parse_key_path(path: &str) -> Result, String> { if path.trim().is_empty() { return Err("keyPath must not be empty".to_string()); @@ -608,6 +689,7 @@ fn override_message(layer: &ConfigLayerSource) -> String { ConfigLayerSource::System { file } => { format!("Overridden by managed config (system): {}", file.display()) } + ConfigLayerSource::AppServerHost => "Overridden by app-server host 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/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 9c10a33408d6..7d7b83d9adee 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -872,6 +872,11 @@ impl MessageProcessor { .read(params) .await .map(|response| Some(response.into())), + ClientRequest::ConfigHostSet { params, .. } => self + .config_processor + .host_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 5327f4ffa2a1..b2e91ee13182 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::ConfigHostSetParams; +use codex_app_server_protocol::ConfigHostSetResponse; use codex_app_server_protocol::ConfigReadParams; use codex_app_server_protocol::ConfigReadResponse; use codex_app_server_protocol::ConfigRequirements; @@ -144,6 +146,19 @@ impl ConfigRequestProcessor { .map(ClientResponsePayload::ConfigBatchWrite) } + pub(crate) async fn host_set( + &self, + params: ConfigHostSetParams, + ) -> Result { + self.handle_config_mutation_result( + self.config_manager + .set_host_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/external_agent_config_processor.rs b/codex-rs/app-server/src/request_processors/external_agent_config_processor.rs index 21b7da679b51..9a232e15eeba 100644 --- a/codex-rs/app-server/src/request_processors/external_agent_config_processor.rs +++ b/codex-rs/app-server/src/request_processors/external_agent_config_processor.rs @@ -80,12 +80,29 @@ impl ExternalAgentConfigRequestProcessor { &self, params: ExternalAgentConfigDetectParams, ) -> Result { + let config = match self + .config_manager + .load_latest_config(/*fallback_cwd*/ None) + .await + { + Ok(config) => Some(config), + Err(err) => { + tracing::warn!( + error = %err, + "external agent config detection could not load app-server config" + ); + None + } + }; let items = self .migration_service - .detect(ExternalAgentConfigDetectOptions { - include_home: params.include_home, - cwds: params.cwds, - }) + .detect_with_config( + ExternalAgentConfigDetectOptions { + include_home: params.include_home, + cwds: params.cwds, + }, + config.as_ref(), + ) .await .map_err(|err| internal_error(err.to_string()))?; diff --git a/codex-rs/config/src/diagnostics.rs b/codex-rs/config/src/diagnostics.rs index 3fc593dcdf68..a30cdbe3e846 100644 --- a/codex-rs/config/src/diagnostics.rs +++ b/codex-rs/config/src/diagnostics.rs @@ -235,6 +235,7 @@ fn config_path_for_layer(layer: &ConfigLayerEntry, config_toml_file: &str) -> Op } ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => Some(file.to_path_buf()), ConfigLayerSource::Mdm { .. } + | ConfigLayerSource::AppServerHost | ConfigLayerSource::SessionFlags | ConfigLayerSource::LegacyManagedConfigTomlFromMdm => None, } diff --git a/codex-rs/config/src/loader/mod.rs b/codex-rs/config/src/loader/mod.rs index 49df306abbb9..ea939c6b2fdf 100644 --- a/codex-rs/config/src/loader/mod.rs +++ b/codex-rs/config/src/loader/mod.rs @@ -120,6 +120,7 @@ pub async fn load_config_layers_state( let ConfigLoadOptions { loader_overrides: overrides, strict_config, + app_server_host_config, } = options.into(); let active_user_profile = overrides.user_config_profile.clone(); let ignore_managed_requirements = overrides.ignore_managed_requirements; @@ -184,7 +185,11 @@ pub async fn load_config_layers_state( .map(AbsolutePathBuf::as_path) .unwrap_or(codex_home); if strict_config { - validate_cli_overrides_strictly(&cli_overrides_layer, base_dir)?; + validate_inline_config_layer_strictly( + &cli_overrides_layer, + base_dir, + "-c/--config override", + )?; } Some(resolve_relative_paths_in_config_toml( cli_overrides_layer, @@ -256,6 +261,22 @@ pub async fn load_config_layers_state( ); } + if let Some(app_server_host_config) = app_server_host_config { + if strict_config { + validate_inline_config_layer_strictly( + &app_server_host_config, + codex_home, + "app-server host config override", + )?; + } + let app_server_host_config = + resolve_relative_paths_in_config_toml(app_server_host_config, codex_home)?; + layers.push(ConfigLayerEntry::new( + ConfigLayerSource::AppServerHost, + app_server_host_config, + )); + } + let mut startup_warnings = None; if let Some(cwd) = cwd { let mut merged_so_far = TomlValue::Table(toml::map::Map::new()); @@ -502,23 +523,23 @@ 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}"), )); } diff --git a/codex-rs/config/src/loader/tests.rs b/codex-rs/config/src/loader/tests.rs index 2d9462bfa781..78d87c9e20b1 100644 --- a/codex-rs/config/src/loader/tests.rs +++ b/codex-rs/config/src/loader/tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::ConfigLayerStackOrdering; use async_trait::async_trait; use codex_file_system::CopyOptions; use codex_file_system::CreateDirectoryOptions; @@ -42,10 +43,18 @@ impl ExecutorFileSystem for TestFileSystem { async fn get_metadata( &self, - _path: &AbsolutePathBuf, + path: &AbsolutePathBuf, _sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult { - unimplemented!("test filesystem only supports reads") + let metadata = tokio::fs::metadata(path.as_path()).await?; + let symlink_metadata = tokio::fs::symlink_metadata(path.as_path()).await?; + Ok(FileMetadata { + is_directory: metadata.is_dir(), + is_file: metadata.is_file(), + is_symlink: symlink_metadata.file_type().is_symlink(), + created_at_ms: 0, + modified_at_ms: 0, + }) } async fn read_directory( @@ -170,3 +179,51 @@ model = "gpt-dev" .await .expect("profile-v2 should allow unrelated legacy profiles in base user config"); } + +#[tokio::test] +async fn app_server_host_project_root_markers_apply_before_project_layer_discovery() { + let tmp = tempdir().expect("tempdir"); + let project_root = tmp.path().join("repo"); + let cwd = project_root.join("nested"); + std::fs::create_dir_all(project_root.join(".codex")).expect("create project config dir"); + std::fs::create_dir_all(&cwd).expect("create cwd"); + std::fs::write(project_root.join("WORKSPACE"), "").expect("write project root marker"); + std::fs::write( + project_root.join(".codex").join(CONFIG_TOML_FILE), + "model = \"gpt-project\"\n", + ) + .expect("write project config"); + + let stack = load_config_layers_state( + &TestFileSystem, + tmp.path(), + Some(AbsolutePathBuf::try_from(cwd).expect("absolute cwd")), + &[], + ConfigLoadOptions { + loader_overrides: LoaderOverrides::without_managed_config_for_tests(), + strict_config: false, + app_server_host_config: Some( + toml::from_str(r#"project_root_markers = ["WORKSPACE"]"#) + .expect("host config toml"), + ), + }, + CloudRequirementsLoader::default(), + &crate::NoopThreadConfigLoader, + ) + .await + .expect("host project markers should load"); + + assert!( + stack + .get_layers( + ConfigLayerStackOrdering::LowestPrecedenceFirst, + /*include_disabled*/ true, + ) + .iter() + .any( + |layer| matches!(&layer.name, ConfigLayerSource::Project { .. }) + && layer.config.get("model").and_then(TomlValue::as_str) == Some("gpt-project") + ), + "host project root markers should expose the ancestor project config layer" + ); +} diff --git a/codex-rs/config/src/state.rs b/codex-rs/config/src/state.rs index 3ec0bfb31494..8369ac39e1d1 100644 --- a/codex-rs/config/src/state.rs +++ b/codex-rs/config/src/state.rs @@ -21,6 +21,7 @@ use toml::Value as TomlValue; pub struct ConfigLoadOptions { pub loader_overrides: LoaderOverrides, pub strict_config: bool, + pub app_server_host_config: Option, } impl From for ConfigLoadOptions { @@ -28,6 +29,7 @@ impl From for ConfigLoadOptions { Self { loader_overrides, strict_config: false, + app_server_host_config: None, } } } @@ -182,6 +184,7 @@ impl ConfigLayerEntry { ConfigLayerSource::Mdm { .. } => None, ConfigLayerSource::System { file } => file.parent(), ConfigLayerSource::User { file, .. } => file.parent(), + ConfigLayerSource::AppServerHost => None, ConfigLayerSource::Project { dot_codex_folder } => Some(dot_codex_folder.clone()), ConfigLayerSource::SessionFlags => None, ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => None, diff --git a/codex-rs/core-skills/src/config_rules.rs b/codex-rs/core-skills/src/config_rules.rs index 92ad2ab1a684..14238c55934a 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::AppServerHost + | ConfigLayerSource::SessionFlags ) { continue; } diff --git a/codex-rs/core-skills/src/loader.rs b/codex-rs/core-skills/src/loader.rs index 2473f7108cf9..171fa712df17 100644 --- a/codex-rs/core-skills/src/loader.rs +++ b/codex-rs/core-skills/src/loader.rs @@ -330,6 +330,7 @@ fn skill_roots_from_layer_stack_inner( }); } ConfigLayerSource::Mdm { .. } + | ConfigLayerSource::AppServerHost | ConfigLayerSource::SessionFlags | ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } | ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {} diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index ff09f26c2845..7ec52d3be642 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1066,6 +1066,7 @@ pub struct ConfigBuilder { harness_overrides: Option, loader_overrides: Option, strict_config: bool, + app_server_host_config: Option, cloud_requirements: CloudRequirementsLoader, thread_config_loader: Option>, fallback_cwd: Option, @@ -1097,6 +1098,11 @@ impl ConfigBuilder { self } + pub fn app_server_host_config(mut self, app_server_host_config: Option) -> Self { + self.app_server_host_config = app_server_host_config; + self + } + pub fn cloud_requirements(mut self, cloud_requirements: CloudRequirementsLoader) -> Self { self.cloud_requirements = cloud_requirements; self @@ -1127,6 +1133,7 @@ impl ConfigBuilder { harness_overrides, loader_overrides, strict_config, + app_server_host_config, cloud_requirements, thread_config_loader, fallback_cwd, @@ -1152,6 +1159,7 @@ impl ConfigBuilder { ConfigLoadOptions { loader_overrides, strict_config, + app_server_host_config, }, cloud_requirements, thread_config_loader diff --git a/codex-rs/core/src/network_proxy_loader.rs b/codex-rs/core/src/network_proxy_loader.rs index dc4b5b6b1652..82d8d19f56a3 100644 --- a/codex-rs/core/src/network_proxy_loader.rs +++ b/codex-rs/core/src/network_proxy_loader.rs @@ -268,6 +268,7 @@ fn is_user_controlled_layer(layer: &ConfigLayerSource) -> bool { matches!( layer, ConfigLayerSource::User { .. } + | ConfigLayerSource::AppServerHost | ConfigLayerSource::Project { .. } | ConfigLayerSource::SessionFlags ) diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 3db7a51576db..c4607ee8131b 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -337,6 +337,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result ConfigLoadOptions { loader_overrides: loader_overrides.clone(), strict_config, + app_server_host_config: None, }, ) .await diff --git a/codex-rs/hooks/src/config_rules.rs b/codex-rs/hooks/src/config_rules.rs index 2935cabc8076..0b8e41f4942e 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::AppServerHost + | ConfigLayerSource::SessionFlags ) { continue; } diff --git a/codex-rs/hooks/src/engine/discovery.rs b/codex-rs/hooks/src/engine/discovery.rs index 3ddf8f23872b..24dc8b372bc3 100644 --- a/codex-rs/hooks/src/engine/discovery.rs +++ b/codex-rs/hooks/src/engine/discovery.rs @@ -364,6 +364,7 @@ fn config_toml_source_path(layer: &ConfigLayerEntry) -> AbsolutePathBuf { ConfigLayerSource::LegacyManagedConfigTomlFromMdm => { synthetic_layer_path("/managed_config.toml") } + ConfigLayerSource::AppServerHost => synthetic_layer_path("/config.toml"), ConfigLayerSource::SessionFlags => synthetic_layer_path("/config.toml"), } } @@ -584,6 +585,7 @@ fn hook_metadata_for_config_layer_source(source: &ConfigLayerSource) -> (HookSou match source { ConfigLayerSource::System { .. } => (HookSource::System, true), ConfigLayerSource::User { .. } => (HookSource::User, false), + ConfigLayerSource::AppServerHost => (HookSource::User, false), ConfigLayerSource::Project { .. } => (HookSource::Project, false), ConfigLayerSource::Mdm { .. } => (HookSource::Mdm, true), ConfigLayerSource::SessionFlags => (HookSource::SessionFlags, false), diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 2075636e3d23..85f85d6ac095 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -255,7 +255,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::AppServerHost | ConfigLayerSource::SessionFlags => { + render_session_flag_details(&layer.config) + } ConfigLayerSource::Mdm { .. } | ConfigLayerSource::LegacyManagedConfigTomlFromMdm => { render_mdm_layer_details(layer) } @@ -388,6 +390,7 @@ fn format_config_layer_source(source: &ConfigLayerSource) -> String { ConfigLayerSource::User { file, .. } => { format!("user ({})", file.as_path().display()) } + ConfigLayerSource::AppServerHost => "app-server-host".to_string(), ConfigLayerSource::Project { dot_codex_folder } => { format!( "project ({}/config.toml)", diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 40cd121c7135..e4cd173c6830 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -958,6 +958,7 @@ pub async fn run_main( codex_config::ConfigLoadOptions { loader_overrides: loader_overrides.clone(), strict_config, + app_server_host_config: None, }, ) .await