From 022c70895fda94a8617b7fa958aa5625e959b80d Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 18 May 2026 21:18:37 -0700 Subject: [PATCH 1/7] feat: add permission profile list api Co-authored-by: Codex noreply@openai.com --- .../schema/json/ClientRequest.json | 52 ++++++ .../codex_app_server_protocol.schemas.json | 96 ++++++++++ .../codex_app_server_protocol.v2.schemas.json | 96 ++++++++++ .../json/v2/PermissionProfileListParams.json | 30 +++ .../v2/PermissionProfileListResponse.json | 44 +++++ .../schema/typescript/ClientRequest.ts | 3 +- .../v2/PermissionProfileListParams.ts | 17 ++ .../v2/PermissionProfileListResponse.ts | 11 ++ .../typescript/v2/PermissionProfileSummary.ts | 13 ++ .../schema/typescript/v2/index.ts | 3 + .../src/protocol/common.rs | 5 + .../src/protocol/v2/permissions.rs | 35 ++++ codex-rs/app-server/README.md | 1 + codex-rs/app-server/src/message_processor.rs | 3 + codex-rs/app-server/src/request_processors.rs | 6 + .../request_processors/catalog_processor.rs | 68 +++++++ .../app-server/tests/common/mcp_process.rs | 10 + codex-rs/app-server/tests/suite/v2/mod.rs | 1 + .../tests/suite/v2/permission_profile_list.rs | 175 ++++++++++++++++++ codex-rs/config/src/permissions_toml.rs | 1 + codex-rs/core/config.schema.json | 3 + codex-rs/core/src/config/config_tests.rs | 22 +++ codex-rs/core/src/config/permissions_tests.rs | 3 + 23 files changed, 697 insertions(+), 1 deletion(-) create mode 100644 codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListResponse.json create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListResponse.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileSummary.ts create mode 100644 codex-rs/app-server/tests/suite/v2/permission_profile_list.rs diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 2621a0bd1eac..1df52717f2cc 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1541,6 +1541,34 @@ ], "type": "string" }, + "PermissionProfileListParams": { + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "cwd": { + "description": "Optional working directory to resolve project config layers.", + "type": [ + "string", + "null" + ] + }, + "limit": { + "description": "Optional page size; defaults to the full result set.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + }, "Personality": { "enum": [ "none", @@ -5324,6 +5352,30 @@ "title": "ExperimentalFeature/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "permissionProfile/list" + ], + "title": "PermissionProfile/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PermissionProfileListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "PermissionProfile/listRequest", + "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 fdaaf3e857d0..ef86c74e2d17 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 @@ -1429,6 +1429,30 @@ "title": "ExperimentalFeature/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "permissionProfile/list" + ], + "title": "PermissionProfile/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/PermissionProfileListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "PermissionProfile/listRequest", + "type": "object" + }, { "properties": { "id": { @@ -11650,6 +11674,78 @@ } ] }, + "PermissionProfileListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "cwd": { + "description": "Optional working directory to resolve project config layers.", + "type": [ + "string", + "null" + ] + }, + "limit": { + "description": "Optional page size; defaults to the full result set.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "title": "PermissionProfileListParams", + "type": "object" + }, + "PermissionProfileListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/v2/PermissionProfileSummary" + }, + "type": "array" + }, + "nextCursor": { + "description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "PermissionProfileListResponse", + "type": "object" + }, + "PermissionProfileSummary": { + "properties": { + "description": { + "description": "Optional user-facing description for display in clients.", + "type": [ + "string", + "null" + ] + }, + "id": { + "description": "Available permission profile identifier.", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, "Personality": { "enum": [ "none", 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 e413d792349c..cd040d50c244 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 @@ -2136,6 +2136,30 @@ "title": "ExperimentalFeature/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "permissionProfile/list" + ], + "title": "PermissionProfile/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PermissionProfileListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "PermissionProfile/listRequest", + "type": "object" + }, { "properties": { "id": { @@ -8199,6 +8223,78 @@ } ] }, + "PermissionProfileListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "cwd": { + "description": "Optional working directory to resolve project config layers.", + "type": [ + "string", + "null" + ] + }, + "limit": { + "description": "Optional page size; defaults to the full result set.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "title": "PermissionProfileListParams", + "type": "object" + }, + "PermissionProfileListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/PermissionProfileSummary" + }, + "type": "array" + }, + "nextCursor": { + "description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "PermissionProfileListResponse", + "type": "object" + }, + "PermissionProfileSummary": { + "properties": { + "description": { + "description": "Optional user-facing description for display in clients.", + "type": [ + "string", + "null" + ] + }, + "id": { + "description": "Available permission profile identifier.", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, "Personality": { "enum": [ "none", diff --git a/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListParams.json b/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListParams.json new file mode 100644 index 000000000000..402dab622168 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListParams.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "cwd": { + "description": "Optional working directory to resolve project config layers.", + "type": [ + "string", + "null" + ] + }, + "limit": { + "description": "Optional page size; defaults to the full result set.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "title": "PermissionProfileListParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListResponse.json new file mode 100644 index 000000000000..4d5a47f8d5d6 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListResponse.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "PermissionProfileSummary": { + "properties": { + "description": { + "description": "Optional user-facing description for display in clients.", + "type": [ + "string", + "null" + ] + }, + "id": { + "description": "Available permission profile identifier.", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "properties": { + "data": { + "items": { + "$ref": "#/definitions/PermissionProfileSummary" + }, + "type": "array" + }, + "nextCursor": { + "description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "PermissionProfileListResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 7f017ed70089..14be7db47aa4 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -42,6 +42,7 @@ import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams"; import type { McpServerToolCallParams } from "./v2/McpServerToolCallParams"; import type { ModelListParams } from "./v2/ModelListParams"; import type { ModelProviderCapabilitiesReadParams } from "./v2/ModelProviderCapabilitiesReadParams"; +import type { PermissionProfileListParams } from "./v2/PermissionProfileListParams"; import type { PluginInstallParams } from "./v2/PluginInstallParams"; import type { PluginInstalledParams } from "./v2/PluginInstalledParams"; import type { PluginListParams } from "./v2/PluginListParams"; @@ -81,4 +82,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/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": "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/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, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListParams.ts new file mode 100644 index 000000000000..24582c9507ad --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListParams.ts @@ -0,0 +1,17 @@ +// 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 PermissionProfileListParams = { +/** + * Opaque pagination cursor returned by a previous call. + */ +cursor?: string | null, +/** + * Optional page size; defaults to the full result set. + */ +limit?: number | null, +/** + * Optional working directory to resolve project config layers. + */ +cwd?: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListResponse.ts new file mode 100644 index 000000000000..ba0ccbc0d60f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListResponse.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 { PermissionProfileSummary } from "./PermissionProfileSummary"; + +export type PermissionProfileListResponse = { data: Array, +/** + * Opaque cursor to pass to the next call to continue after the last item. + * If None, there are no more items to return. + */ +nextCursor: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileSummary.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileSummary.ts new file mode 100644 index 000000000000..9d02fd776b4e --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileSummary.ts @@ -0,0 +1,13 @@ +// 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 PermissionProfileSummary = { +/** + * Available permission profile identifier. + */ +id: string, +/** + * Optional user-facing description for display in clients. + */ +description: string | null, }; 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 dc400a6f8efa..deed91a4bca5 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -255,6 +255,9 @@ export type { OverriddenMetadata } from "./OverriddenMetadata"; export type { PatchApplyStatus } from "./PatchApplyStatus"; export type { PatchChangeKind } from "./PatchChangeKind"; export type { PermissionGrantScope } from "./PermissionGrantScope"; +export type { PermissionProfileListParams } from "./PermissionProfileListParams"; +export type { PermissionProfileListResponse } from "./PermissionProfileListResponse"; +export type { PermissionProfileSummary } from "./PermissionProfileSummary"; export type { PermissionsRequestApprovalParams } from "./PermissionsRequestApprovalParams"; export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse"; export type { PlanDeltaNotification } from "./PlanDeltaNotification"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 7a7fe5642c99..4adcf5f5927f 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -800,6 +800,11 @@ client_request_definitions! { serialization: global("config"), response: v2::ExperimentalFeatureListResponse, }, + PermissionProfileList => "permissionProfile/list" { + params: v2::PermissionProfileListParams, + serialization: global_shared_read("config"), + response: v2::PermissionProfileListResponse, + }, ExperimentalFeatureEnablementSet => "experimentalFeature/enablement/set" { params: v2::ExperimentalFeatureEnablementSetParams, serialization: global("config"), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs b/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs index f00bcfaefb31..0514c0087dd7 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs @@ -287,6 +287,41 @@ impl From for CoreFileSystemSandboxEntry { } } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PermissionProfileListParams { + /// Opaque pagination cursor returned by a previous call. + #[ts(optional = nullable)] + pub cursor: Option, + /// Optional page size; defaults to the full result set. + #[ts(optional = nullable)] + pub limit: Option, + /// Optional working directory to resolve project config layers. + #[ts(optional = nullable)] + pub cwd: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PermissionProfileSummary { + /// Available permission profile identifier. + pub id: String, + /// Optional user-facing description for display in clients. + pub description: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PermissionProfileListResponse { + pub data: Vec, + /// Opaque cursor to pass to the next call to continue after the last item. + /// If None, there are no more items to return. + pub next_cursor: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[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 9073282b4e42..599799b79558 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -189,6 +189,7 @@ Example with notification opt-out: - `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata. - `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider. - `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. Pass `threadId` when showing feature state for an existing loaded thread so `enabled` is computed from that thread's refreshed config, including project-local config for the thread's cwd; if omitted, the server uses its default config resolution context. For non-beta flags, `displayName`/`description`/`announcement` are `null`. +- `permissionProfile/list` — list available permission profile ids with optional display `description` text, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.]` entries to be included in the current catalog view. - `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `memories`, `plugins`, `tool_suggest`, `tool_call_mcp_elicitation`). For each feature, precedence is: cloud requirements > --enable > config.toml > experimentalFeature/enablement/set (new) > code default. - `environment/add` — experimental; add or replace a named remote environment by `environmentId` and `execServerUrl` for later selection by `thread/start` or `turn/start`; returns `{}` and does not change the default environment. - `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). Built-in presets do not select a model; the Plan preset selects medium reasoning effort. This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly. diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 4e2c6f38cc31..b5cfcccf61d2 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -1148,6 +1148,9 @@ impl MessageProcessor { .experimental_feature_list(params) .await } + ClientRequest::PermissionProfileList { params, .. } => { + self.catalog_processor.permission_profile_list(params).await + } ClientRequest::CollaborationModeList { params, .. } => { self.catalog_processor.collaboration_mode_list(params).await } diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index 305e864ba0e9..d0996d985ada 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -103,6 +103,9 @@ use codex_app_server_protocol::MockExperimentalMethodParams; use codex_app_server_protocol::MockExperimentalMethodResponse; use codex_app_server_protocol::ModelListParams; use codex_app_server_protocol::ModelListResponse; +use codex_app_server_protocol::PermissionProfileListParams; +use codex_app_server_protocol::PermissionProfileListResponse; +use codex_app_server_protocol::PermissionProfileSummary; use codex_app_server_protocol::PluginDetail; use codex_app_server_protocol::PluginInstallParams; use codex_app_server_protocol::PluginInstallResponse; @@ -356,6 +359,9 @@ use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; #[cfg(test)] use codex_protocol::items::TurnItem; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; use codex_protocol::models::ResponseItem; #[cfg(test)] use codex_protocol::permissions::FileSystemSandboxPolicy; diff --git a/codex-rs/app-server/src/request_processors/catalog_processor.rs b/codex-rs/app-server/src/request_processors/catalog_processor.rs index 834f70654dc8..7b958118f50c 100644 --- a/codex-rs/app-server/src/request_processors/catalog_processor.rs +++ b/codex-rs/app-server/src/request_processors/catalog_processor.rs @@ -1,4 +1,5 @@ use super::*; +use codex_config::config_toml::ConfigToml; use futures::StreamExt; #[derive(Clone)] @@ -155,6 +156,15 @@ impl CatalogRequestProcessor { .map(|response| Some(response.into())) } + pub(crate) async fn permission_profile_list( + &self, + params: PermissionProfileListParams, + ) -> Result, JSONRPCErrorError> { + self.permission_profile_list_response(params) + .await + .map(|response| Some(response.into())) + } + pub(crate) async fn collaboration_mode_list( &self, params: CollaborationModeListParams, @@ -389,6 +399,64 @@ impl CatalogRequestProcessor { Ok(ExperimentalFeatureListResponse { data, next_cursor }) } + async fn permission_profile_list_response( + &self, + params: PermissionProfileListParams, + ) -> Result { + let PermissionProfileListParams { cursor, limit, cwd } = params; + let config = self.load_latest_config(cwd.map(PathBuf::from)).await?; + let effective_config: ConfigToml = config + .config_layer_stack + .effective_config() + .try_into() + .map_err(|err| internal_error(format!("failed to read effective config: {err}")))?; + let mut profiles = vec![ + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), + description: None, + }, + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), + description: None, + }, + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), + description: None, + }, + ]; + profiles.extend( + effective_config + .permissions + .into_iter() + .flat_map(|permissions| permissions.entries) + .map(|(id, profile)| PermissionProfileSummary { + id, + description: profile.description, + }), + ); + let total = profiles.len(); + let effective_limit = limit.unwrap_or(total as u32).max(1) as usize; + let effective_limit = effective_limit.min(total); + let start = match cursor { + Some(cursor) => cursor + .parse::() + .map_err(|_| invalid_request(format!("invalid cursor: {cursor}")))?, + None => 0, + }; + + if start > total { + return Err(invalid_request(format!( + "cursor {start} exceeds total permission profiles {total}" + ))); + } + + let end = start.saturating_add(effective_limit).min(total); + let data = profiles[start..end].to_vec(); + let next_cursor = (end < total).then_some(end.to_string()); + + Ok(PermissionProfileListResponse { data, next_cursor }) + } + async fn mock_experimental_method_inner( &self, params: MockExperimentalMethodParams, diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs index 3445010a7fca..115764f822fd 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -56,6 +56,7 @@ use codex_app_server_protocol::McpServerToolCallParams; use codex_app_server_protocol::MockExperimentalMethodParams; use codex_app_server_protocol::ModelListParams; use codex_app_server_protocol::ModelProviderCapabilitiesReadParams; +use codex_app_server_protocol::PermissionProfileListParams; use codex_app_server_protocol::PluginInstallParams; use codex_app_server_protocol::PluginInstalledParams; use codex_app_server_protocol::PluginListParams; @@ -561,6 +562,15 @@ impl McpProcess { self.send_request("experimentalFeature/list", params).await } + /// Send a `permissionProfile/list` JSON-RPC request. + pub async fn send_permission_profile_list_request( + &mut self, + params: PermissionProfileListParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("permissionProfile/list", params).await + } + /// Send an `experimentalFeature/enablement/set` JSON-RPC request. pub async fn send_experimental_feature_enablement_set_request( &mut self, diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index bdbe7b7ddd56..1c81fe1ed6cc 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -29,6 +29,7 @@ mod memory_reset; mod model_list; mod model_provider_capabilities_read; mod output_schema; +mod permission_profile_list; mod plan_item; mod plugin_install; mod plugin_list; diff --git a/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs b/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs new file mode 100644 index 000000000000..8d22c8b85d06 --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs @@ -0,0 +1,175 @@ +use std::time::Duration; + +use anyhow::Result; +use app_test_support::McpProcess; +use app_test_support::to_response; +use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::PermissionProfileListParams; +use codex_app_server_protocol::PermissionProfileListResponse; +use codex_app_server_protocol::PermissionProfileSummary; +use codex_app_server_protocol::RequestId; +use codex_core::config::set_project_trust_level; +use codex_protocol::config_types::TrustLevel; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; +use pretty_assertions::assert_eq; +use tempfile::TempDir; +use tokio::time::timeout; + +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); + +#[tokio::test] +async fn permission_profile_list_returns_builtin_and_configured_profiles() -> Result<()> { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join("config.toml"), + r#" +default_permissions = "dev" + +[permissions.dev] +description = "Day-to-day coding work." + +[permissions.dev.filesystem] +":workspace_roots" = "write" + +[permissions.audit] +description = "Inspect without writes." + +[permissions.audit.filesystem] +":workspace_roots" = "read" +"#, + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_permission_profile_list_request(PermissionProfileListParams { + cursor: None, + limit: None, + cwd: None, + }) + .await?; + let actual = read_response::(&mut mcp, request_id).await?; + + assert_eq!( + actual, + PermissionProfileListResponse { + data: vec![ + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), + description: None, + }, + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), + description: None, + }, + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), + description: None, + }, + PermissionProfileSummary { + id: "audit".to_string(), + description: Some("Inspect without writes.".to_string()), + }, + PermissionProfileSummary { + id: "dev".to_string(), + description: Some("Day-to-day coding work.".to_string()), + }, + ], + next_cursor: None, + } + ); + Ok(()) +} + +#[tokio::test] +async fn permission_profile_list_resolves_project_profiles_and_paginates() -> Result<()> { + let codex_home = TempDir::new()?; + let workspace = TempDir::new()?; + let project_config_dir = workspace.path().join(".codex"); + std::fs::create_dir_all(&project_config_dir)?; + std::fs::write( + codex_home.path().join("config.toml"), + r#" +default_permissions = ":workspace" +"#, + )?; + std::fs::write( + project_config_dir.join("config.toml"), + r#" +[permissions.project] +description = "Project-scoped profile." + +[permissions.project.filesystem] +":workspace_roots" = "write" +"#, + )?; + set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let first_request_id = mcp + .send_permission_profile_list_request(PermissionProfileListParams { + cursor: None, + limit: Some(3), + cwd: Some(workspace.path().to_string_lossy().into_owned()), + }) + .await?; + let first = read_response::(&mut mcp, first_request_id).await?; + assert_eq!( + first, + PermissionProfileListResponse { + data: vec![ + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), + description: None, + }, + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), + description: None, + }, + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), + description: None, + }, + ], + next_cursor: Some("3".to_string()), + } + ); + + let second_request_id = mcp + .send_permission_profile_list_request(PermissionProfileListParams { + cursor: first.next_cursor, + limit: Some(3), + cwd: Some(workspace.path().to_string_lossy().into_owned()), + }) + .await?; + let second = + read_response::(&mut mcp, second_request_id).await?; + assert_eq!( + second, + PermissionProfileListResponse { + data: vec![PermissionProfileSummary { + id: "project".to_string(), + description: Some("Project-scoped profile.".to_string()), + }], + next_cursor: None, + } + ); + Ok(()) +} + +async fn read_response( + mcp: &mut McpProcess, + request_id: i64, +) -> Result { + let response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + to_response(response) +} diff --git a/codex-rs/config/src/permissions_toml.rs b/codex-rs/config/src/permissions_toml.rs index fff8c677061b..4d533776ad0f 100644 --- a/codex-rs/config/src/permissions_toml.rs +++ b/codex-rs/config/src/permissions_toml.rs @@ -25,6 +25,7 @@ impl PermissionsToml { #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct PermissionProfileToml { + pub description: Option, pub workspace_roots: Option, pub filesystem: Option, pub network: Option, diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 07f49a3cd630..e8092bf300e3 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1966,6 +1966,9 @@ "PermissionProfileToml": { "additionalProperties": false, "properties": { + "description": { + "type": "string" + }, "filesystem": { "$ref": "#/definitions/FilesystemPermissionsToml" }, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 50b03b5051ca..8314c43b89c5 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -734,6 +734,9 @@ fn config_toml_deserializes_permission_profiles() { let toml = r#" default_permissions = "workspace" +[permissions.workspace] +description = "Day-to-day workspace access." + [permissions.workspace.workspace_roots] "~/code/openai" = true "~/code/ignored" = false @@ -764,6 +767,7 @@ allow_upstream_proxy = false entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: Some("Day-to-day workspace access.".to_string()), workspace_roots: Some(WorkspaceRootsToml { entries: BTreeMap::from([ ("~/code/ignored".to_string(), false), @@ -825,6 +829,7 @@ async fn permissions_profiles_proxy_policy_does_not_start_managed_network_proxy_ entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, @@ -979,6 +984,7 @@ async fn network_proxy_feature_matrix_preserves_sandbox_network_semantics() -> s entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, @@ -1130,6 +1136,7 @@ async fn network_proxy_feature_uses_profile_network_proxy_settings() -> std::io: entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, @@ -1233,6 +1240,7 @@ enabled = false entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, @@ -1282,6 +1290,7 @@ async fn permissions_profiles_network_disabled_by_default_does_not_start_proxy() entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, @@ -1329,6 +1338,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std:: entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, @@ -1633,6 +1643,7 @@ async fn permission_profile_override_preserves_configured_network_policy_without entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, @@ -1693,6 +1704,7 @@ async fn workspace_root_glob_none_compiles_to_filesystem_pattern_entry() -> std: entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: Some(2), @@ -1772,6 +1784,7 @@ async fn permissions_profiles_require_default_permissions() -> std::io::Result<( entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, @@ -1897,6 +1910,7 @@ async fn workspace_profile_applies_rules_to_runtime_and_profile_workspace_roots( entries: BTreeMap::from([( "dev".to_string(), PermissionProfileToml { + description: None, workspace_roots: Some(WorkspaceRootsToml { entries: BTreeMap::from([( profile_root.to_string_lossy().into_owned(), @@ -2355,6 +2369,7 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root() entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, @@ -2412,6 +2427,7 @@ async fn permissions_profiles_reject_nested_entries_for_non_workspace_roots() -> entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, @@ -2473,6 +2489,7 @@ async fn load_workspace_permission_profile( #[tokio::test] async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<()> { let config = load_workspace_permission_profile(PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, @@ -2517,6 +2534,7 @@ async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<( async fn permissions_profiles_allow_unknown_special_paths_with_nested_entries() -> std::io::Result<()> { let config = load_workspace_permission_profile(PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, @@ -2554,6 +2572,7 @@ async fn permissions_profiles_allow_unknown_special_paths_with_nested_entries() #[tokio::test] async fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io::Result<()> { let config = load_workspace_permission_profile(PermissionProfileToml { + description: None, workspace_roots: None, filesystem: None, network: None, @@ -2583,6 +2602,7 @@ async fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io #[tokio::test] async fn permissions_profiles_allow_empty_filesystem_with_warning() -> std::io::Result<()> { let config = load_workspace_permission_profile(PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, @@ -2619,6 +2639,7 @@ async fn permissions_profiles_reject_workspace_root_parent_traversal() -> std::i entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, @@ -2666,6 +2687,7 @@ async fn permissions_profiles_allow_network_enablement() -> std::io::Result<()> entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, diff --git a/codex-rs/core/src/config/permissions_tests.rs b/codex-rs/core/src/config/permissions_tests.rs index 51cf13912e7f..bc7349a63c18 100644 --- a/codex-rs/core/src/config/permissions_tests.rs +++ b/codex-rs/core/src/config/permissions_tests.rs @@ -67,6 +67,7 @@ async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Resu entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, @@ -285,6 +286,7 @@ fn compile_permission_profile_workspace_roots_resolves_enabled_entries() -> std: entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: None, workspace_roots: Some(WorkspaceRootsToml { entries: BTreeMap::from([ ("backend".to_string(), true), @@ -394,6 +396,7 @@ fn read_write_trailing_glob_suffix_compiles_as_subpath() -> std::io::Result<()> entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { + description: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, From 05c815cae4e9d121191802b44b7cbe86eb0301e5 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 18 May 2026 21:53:44 -0700 Subject: [PATCH 2/7] feat(config): support managed permission profiles in requirements Co-authored-by: Codex noreply@openai.com --- .../codex_app_server_protocol.schemas.json | 15 ++ .../codex_app_server_protocol.v2.schemas.json | 15 ++ .../v2/ConfigRequirementsReadResponse.json | 15 ++ .../typescript/v2/ConfigRequirements.ts | 2 +- .../src/protocol/v2/config.rs | 2 + .../src/protocol/v2/tests.rs | 2 + codex-rs/app-server/README.md | 4 +- .../request_processors/catalog_processor.rs | 125 ++++++++++++--- .../request_processors/config_processor.rs | 18 +++ codex-rs/cloud-requirements/src/lib.rs | 32 ++++ codex-rs/config/src/config_requirements.rs | 84 ++++++++++ .../core/src/config/config_loader_tests.rs | 64 ++++++++ codex-rs/core/src/config/config_tests.rs | 4 + codex-rs/core/src/config/mod.rs | 143 +++++++++++++++++- codex-rs/tui/src/debug_config.rs | 4 + 15 files changed, 495 insertions(+), 34 deletions(-) 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 ef86c74e2d17..63454cb04688 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 @@ -7612,6 +7612,15 @@ "null" ] }, + "allowedPermissions": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, "allowedSandboxModes": { "items": { "$ref": "#/definitions/v2/SandboxMode" @@ -7630,6 +7639,12 @@ "null" ] }, + "defaultPermissions": { + "type": [ + "string", + "null" + ] + }, "enforceResidency": { "anyOf": [ { 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 cd040d50c244..4d133964b752 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 @@ -4001,6 +4001,15 @@ "null" ] }, + "allowedPermissions": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, "allowedSandboxModes": { "items": { "$ref": "#/definitions/SandboxMode" @@ -4019,6 +4028,12 @@ "null" ] }, + "defaultPermissions": { + "type": [ + "string", + "null" + ] + }, "enforceResidency": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index 41bd337decb1..c4bf18e81e0a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -77,6 +77,15 @@ "null" ] }, + "allowedPermissions": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, "allowedSandboxModes": { "items": { "$ref": "#/definitions/SandboxMode" @@ -95,6 +104,12 @@ "null" ] }, + "defaultPermissions": { + "type": [ + "string", + "null" + ] + }, "enforceResidency": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts index 1b0625e5e16e..2456adfcf88f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts @@ -6,4 +6,4 @@ import type { AskForApproval } from "./AskForApproval"; import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { SandboxMode } from "./SandboxMode"; -export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; +export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, defaultPermissions: string | null, allowedPermissions: Array | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; 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 b46515d81142..ad956459aae9 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -384,6 +384,8 @@ pub struct ConfigRequirements { #[experimental("configRequirements/read.allowedApprovalsReviewers")] pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, + pub default_permissions: Option, + pub allowed_permissions: Option>, pub allowed_web_search_modes: Option>, pub allow_managed_hooks_only: Option, pub feature_requirements: Option>, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index b445fc4d01af..676a0d674413 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -1697,6 +1697,8 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() }]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, feature_requirements: None, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 599799b79558..deaf664b6133 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -189,7 +189,7 @@ Example with notification opt-out: - `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata. - `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider. - `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. Pass `threadId` when showing feature state for an existing loaded thread so `enabled` is computed from that thread's refreshed config, including project-local config for the thread's cwd; if omitted, the server uses its default config resolution context. For non-beta flags, `displayName`/`description`/`announcement` are `null`. -- `permissionProfile/list` — list available permission profile ids with optional display `description` text, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.]` entries to be included in the current catalog view. +- `permissionProfile/list` — list available permission profile ids with optional display `description` text, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.]` entries to be included in the current catalog view. Managed `requirements.toml` profiles are included too, and `allowed_permissions` filters the returned catalog when present. - `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `memories`, `plugins`, `tool_suggest`, `tool_call_mcp_elicitation`). For each feature, precedence is: cloud requirements > --enable > config.toml > experimentalFeature/enablement/set (new) > code default. - `environment/add` — experimental; add or replace a named remote environment by `environmentId` and `execServerUrl` for later selection by `thread/start` or `turn/start`; returns `{}` and does not change the default environment. - `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). Built-in presets do not select a model; the Plan preset selects medium reasoning effort. This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly. @@ -224,7 +224,7 @@ Example with notification opt-out: - `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`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. +- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), permission-profile defaults/catalogs (`defaultPermissions`, `allowedPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. ### Example: Start or resume a thread diff --git a/codex-rs/app-server/src/request_processors/catalog_processor.rs b/codex-rs/app-server/src/request_processors/catalog_processor.rs index 7b958118f50c..1b043875d05c 100644 --- a/codex-rs/app-server/src/request_processors/catalog_processor.rs +++ b/codex-rs/app-server/src/request_processors/catalog_processor.rs @@ -1,5 +1,7 @@ use super::*; +use codex_config::ConfigRequirementsToml; use codex_config::config_toml::ConfigToml; +use codex_config::permissions_toml::PermissionsToml; use futures::StreamExt; #[derive(Clone)] @@ -410,29 +412,9 @@ impl CatalogRequestProcessor { .effective_config() .try_into() .map_err(|err| internal_error(format!("failed to read effective config: {err}")))?; - let mut profiles = vec![ - PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), - description: None, - }, - PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), - description: None, - }, - PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), - description: None, - }, - ]; - profiles.extend( - effective_config - .permissions - .into_iter() - .flat_map(|permissions| permissions.entries) - .map(|(id, profile)| PermissionProfileSummary { - id, - description: profile.description, - }), + let profiles = permission_profile_summaries( + effective_config.permissions.as_ref(), + config.config_layer_stack.requirements_toml(), ); let total = profiles.len(); let effective_limit = limit.unwrap_or(total as u32).max(1) as usize; @@ -667,3 +649,100 @@ impl CatalogRequestProcessor { .map_err(|err| internal_error(format!("failed to update skill settings: {err}"))) } } + +fn permission_profile_summaries( + config_permissions: Option<&PermissionsToml>, + requirements_toml: &ConfigRequirementsToml, +) -> Vec { + let mut profiles = vec![ + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), + description: None, + }, + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), + description: None, + }, + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), + description: None, + }, + ]; + profiles.extend( + config_permissions + .into_iter() + .flat_map(|permissions| permissions.entries.iter()) + .map(|(id, profile)| PermissionProfileSummary { + id: id.clone(), + description: profile.description.clone(), + }), + ); + profiles.extend( + requirements_toml + .permissions + .as_ref() + .into_iter() + .flat_map(|permissions| permissions.profiles.iter()) + .map(|(id, profile)| PermissionProfileSummary { + id: id.clone(), + description: profile.description.clone(), + }), + ); + if let Some(allowed_permissions) = requirements_toml.allowed_permissions.as_ref() { + profiles.retain(|profile| { + allowed_permissions + .iter() + .any(|allowed_profile| allowed_profile == &profile.id) + }); + } + profiles +} + +#[cfg(test)] +mod tests { + use super::PermissionProfileSummary; + use super::permission_profile_summaries; + use codex_config::ConfigRequirementsToml; + use codex_config::permissions_toml::PermissionProfileToml; + use codex_config::permissions_toml::PermissionsToml; + use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY; + use pretty_assertions::assert_eq; + use std::collections::BTreeMap; + use toml::from_str; + + #[test] + fn permission_profile_summaries_include_managed_profiles_and_apply_allowlist() { + let config_permissions = PermissionsToml { + entries: BTreeMap::from([( + "local".to_string(), + PermissionProfileToml { + description: Some("Local profile.".to_string()), + ..PermissionProfileToml::default() + }, + )]), + }; + let requirements_toml: ConfigRequirementsToml = from_str( + r#" + allowed_permissions = [":read-only", "managed-standard"] + + [permissions.managed-standard] + description = "Managed profile." + "#, + ) + .expect("requirements TOML should parse"); + + assert_eq!( + permission_profile_summaries(Some(&config_permissions), &requirements_toml), + vec![ + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), + description: None, + }, + PermissionProfileSummary { + id: "managed-standard".to_string(), + description: Some("Managed profile.".to_string()), + }, + ] + ); + } +} 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 5054ea248d67..e43728e01aef 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -418,6 +418,8 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR .filter_map(map_sandbox_mode_requirement_to_api) .collect() }), + default_permissions: requirements.default_permissions, + allowed_permissions: requirements.allowed_permissions, allowed_web_search_modes: requirements.allowed_web_search_modes.map(|modes| { let mut normalized = modes .into_iter() @@ -621,10 +623,26 @@ mod tests { #[test] fn requirements_api_includes_allow_managed_hooks_only() { let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { + default_permissions: Some("managed-standard".to_string()), + allowed_permissions: Some(vec![ + "managed-standard".to_string(), + "managed-build".to_string(), + ]), allow_managed_hooks_only: Some(true), ..ConfigRequirementsToml::default() }); + assert_eq!( + mapped.default_permissions, + Some("managed-standard".to_string()) + ); + assert_eq!( + mapped.allowed_permissions, + Some(vec![ + "managed-standard".to_string(), + "managed-build".to_string(), + ]) + ); assert_eq!(mapped.allow_managed_hooks_only, Some(true)); assert_eq!(mapped.hooks, None); } diff --git a/codex-rs/cloud-requirements/src/lib.rs b/codex-rs/cloud-requirements/src/lib.rs index 8812762cbed2..992ab25cef05 100644 --- a/codex-rs/cloud-requirements/src/lib.rs +++ b/codex-rs/cloud-requirements/src/lib.rs @@ -1202,6 +1202,8 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1285,6 +1287,8 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1319,6 +1323,8 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1370,6 +1376,8 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1521,6 +1529,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1602,6 +1612,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1681,6 +1693,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1888,6 +1902,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1929,6 +1945,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1990,6 +2008,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2047,6 +2067,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2106,6 +2128,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2166,6 +2190,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2226,6 +2252,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2316,6 +2344,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2348,6 +2378,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 4e4336a2b505..48d0ec5b9097 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -19,6 +19,7 @@ use crate::Constrained; use crate::ConstraintError; use crate::ManagedHooksRequirementsToml; use crate::mcp_types::AppToolApproval; +use crate::permissions_toml::PermissionProfileToml; #[derive(Debug, Clone, PartialEq, Eq)] pub enum RequirementSource { @@ -438,6 +439,8 @@ pub struct FilesystemRequirementsToml { #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct PermissionsRequirementsToml { pub filesystem: Option, + #[serde(default, flatten)] + pub profiles: BTreeMap, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -688,6 +691,8 @@ pub struct ConfigRequirementsToml { pub allowed_approval_policies: Option>, pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, + pub default_permissions: Option, + pub allowed_permissions: Option>, pub remote_sandbox_config: Option>, pub allowed_web_search_modes: Option>, pub allow_managed_hooks_only: Option, @@ -738,6 +743,8 @@ pub struct ConfigRequirementsWithSources { pub allowed_approval_policies: Option>>, pub allowed_approvals_reviewers: Option>>, pub allowed_sandbox_modes: Option>>, + pub default_permissions: Option>, + pub allowed_permissions: Option>>, pub allowed_web_search_modes: Option>>, pub allow_managed_hooks_only: Option>, pub feature_requirements: Option>, @@ -774,6 +781,8 @@ impl ConfigRequirementsWithSources { allowed_approval_policies: _, allowed_approvals_reviewers: _, allowed_sandbox_modes: _, + default_permissions: _, + allowed_permissions: _, remote_sandbox_config: _, allowed_web_search_modes: _, allow_managed_hooks_only: _, @@ -805,6 +814,8 @@ impl ConfigRequirementsWithSources { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, + default_permissions, + allowed_permissions, allowed_web_search_modes, allow_managed_hooks_only, feature_requirements, @@ -833,6 +844,8 @@ impl ConfigRequirementsWithSources { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, + default_permissions, + allowed_permissions, allowed_web_search_modes, allow_managed_hooks_only, feature_requirements, @@ -850,6 +863,8 @@ impl ConfigRequirementsWithSources { allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value), allowed_approvals_reviewers: allowed_approvals_reviewers.map(|sourced| sourced.value), allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value), + default_permissions: default_permissions.map(|sourced| sourced.value), + allowed_permissions: allowed_permissions.map(|sourced| sourced.value), remote_sandbox_config: None, allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value), allow_managed_hooks_only: allow_managed_hooks_only.map(|sourced| sourced.value), @@ -934,6 +949,8 @@ impl ConfigRequirementsToml { self.allowed_approval_policies.is_none() && self.allowed_approvals_reviewers.is_none() && self.allowed_sandbox_modes.is_none() + && self.default_permissions.is_none() + && self.allowed_permissions.is_none() && self.remote_sandbox_config.is_none() && self.allowed_web_search_modes.is_none() && self.allow_managed_hooks_only.is_none() @@ -973,6 +990,8 @@ impl TryFrom for ConfigRequirements { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, + default_permissions: _default_permissions, + allowed_permissions: _allowed_permissions, allowed_web_search_modes, allow_managed_hooks_only, feature_requirements, @@ -1276,6 +1295,8 @@ mod tests { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, + default_permissions, + allowed_permissions, remote_sandbox_config: _, allowed_web_search_modes, allow_managed_hooks_only, @@ -1297,6 +1318,10 @@ mod tests { .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_sandbox_modes: allowed_sandbox_modes .map(|value| Sourced::new(value, RequirementSource::Unknown)), + default_permissions: default_permissions + .map(|value| Sourced::new(value, RequirementSource::Unknown)), + allowed_permissions: allowed_permissions + .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_web_search_modes: allowed_web_search_modes .map(|value| Sourced::new(value, RequirementSource::Unknown)), allow_managed_hooks_only: allow_managed_hooks_only @@ -1343,6 +1368,54 @@ mod tests { Ok(()) } + #[test] + fn deserialize_managed_permission_profiles() -> Result<()> { + let requirements: ConfigRequirementsToml = from_str( + r#" + default_permissions = "managed-standard" + allowed_permissions = ["managed-standard", "managed-build"] + + [permissions.managed-standard] + description = "General managed access." + + [permissions.managed-build.filesystem] + ":workspace_roots" = "write" + "#, + )?; + + assert_eq!( + requirements.default_permissions.as_deref(), + Some("managed-standard") + ); + assert_eq!( + requirements.allowed_permissions, + Some(vec![ + "managed-standard".to_string(), + "managed-build".to_string(), + ]) + ); + let permissions = requirements + .permissions + .as_ref() + .expect("managed permission profiles"); + assert_eq!( + permissions + .profiles + .get("managed-standard") + .and_then(|profile| profile.description.as_deref()), + Some("General managed access.") + ); + assert!( + permissions + .profiles + .get("managed-build") + .and_then(|profile| profile.filesystem.as_ref()) + .is_some() + ); + assert!(!requirements.is_empty()); + Ok(()) + } + #[test] fn merge_unset_fields_copies_every_field_and_sets_sources() { let mut target = ConfigRequirementsWithSources::default(); @@ -1372,6 +1445,8 @@ mod tests { allowed_approval_policies: Some(allowed_approval_policies.clone()), allowed_approvals_reviewers: Some(allowed_approvals_reviewers.clone()), allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()), + default_permissions: Some("managed".to_string()), + allowed_permissions: Some(vec!["managed".to_string()]), remote_sandbox_config: None, allowed_web_search_modes: Some(allowed_web_search_modes.clone()), allow_managed_hooks_only: Some(true), @@ -1401,6 +1476,11 @@ mod tests { source.clone(), )), allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source.clone(),)), + default_permissions: Some(Sourced::new("managed".to_string(), source.clone(),)), + allowed_permissions: Some(Sourced::new( + vec!["managed".to_string()], + source.clone(), + )), allowed_web_search_modes: Some(Sourced::new( allowed_web_search_modes, enforce_source.clone(), @@ -1450,6 +1530,8 @@ mod tests { )), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, feature_requirements: None, @@ -1498,6 +1580,8 @@ mod tests { )), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, feature_requirements: None, diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index cd84f06cda00..6960dcf995b3 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -1095,6 +1095,8 @@ allowed_approval_policies = ["on-request"] allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1153,6 +1155,8 @@ allowed_approval_policies = ["on-request"] allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1360,6 +1364,8 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()> allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1408,6 +1414,64 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()> Ok(()) } +#[tokio::test] +async fn system_requirements_define_managed_permission_profiles() -> anyhow::Result<()> { + let tmp = tempdir()?; + let codex_home = tmp.path().join("home"); + tokio::fs::create_dir_all(&codex_home).await?; + tokio::fs::write( + codex_home.join(CONFIG_TOML_FILE), + r#" +default_permissions = "local" + +[permissions.local.filesystem] +":workspace_roots" = "read" +"#, + ) + .await?; + let requirements_path = tmp.path().join("requirements.toml"); + tokio::fs::write( + &requirements_path, + r#" +default_permissions = "managed-standard" +allowed_permissions = ["managed-standard"] + +[permissions.managed-standard] +description = "Managed profile." + +[permissions.managed-standard.filesystem] +":workspace_roots" = "read" +"#, + ) + .await?; + + let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?; + let mut overrides = LoaderOverrides::without_managed_config_for_tests(); + overrides.system_requirements_path = Some(requirements_path); + let config = ConfigBuilder::default() + .codex_home(codex_home) + .fallback_cwd(Some(cwd.to_path_buf())) + .loader_overrides(overrides) + .build() + .await?; + + assert_eq!( + config + .config_layer_stack + .requirements_toml() + .allowed_permissions, + Some(vec!["managed-standard".to_string()]) + ); + assert_eq!( + config + .permissions + .active_permission_profile() + .map(|profile| profile.id), + Some("managed-standard".to_string()) + ); + Ok(()) +} + #[tokio::test] async fn load_config_layers_can_ignore_managed_requirements() -> anyhow::Result<()> { let tmp = tempdir()?; diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 8314c43b89c5..36b27812ac05 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -8632,6 +8632,8 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() allowed_approval_policies: None, allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]), allow_managed_hooks_only: None, @@ -9415,6 +9417,8 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s allowed_approval_policies: None, allowed_approvals_reviewers: None, allowed_sandbox_modes: Some(vec![codex_config::SandboxModeRequirement::ReadOnly]), + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 583c2a42b6f1..f92bdd50cd9e 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -33,6 +33,7 @@ use codex_config::config_toml::ThreadStoreToml; use codex_config::config_toml::validate_model_providers; use codex_config::loader::load_config_layers_state; use codex_config::loader::project_trust_key; +use codex_config::permissions_toml::PermissionsToml; use codex_config::profile_toml::ConfigProfile; use codex_config::sandbox_mode_requirement_for_permission_profile; use codex_config::types::ApprovalsReviewer; @@ -120,6 +121,7 @@ use crate::config::permissions::compile_permission_profile_selection; use crate::config::permissions::compile_permission_profile_workspace_roots; use crate::config::permissions::default_builtin_permission_profile_name; use crate::config::permissions::get_readable_roots_required_for_codex_runtime; +use crate::config::permissions::is_builtin_permission_profile_name; use crate::config::permissions::network_proxy_config_for_profile_selection; use crate::config::permissions::validate_user_permission_profile_names; use crate::config_lock::config_without_lock_controls; @@ -2568,14 +2570,49 @@ impl Config { sandbox_mode, config_profile.sandbox_mode, ); - let has_permission_profiles = cfg - .permissions + let requirements_toml = config_layer_stack.requirements_toml(); + let effective_permissions = + merge_managed_permission_profiles(cfg.permissions.as_ref(), requirements_toml)?; + validate_user_permission_profile_names(effective_permissions.as_ref())?; + validate_required_permission_profile_catalog( + requirements_toml, + effective_permissions.as_ref(), + )?; + + let has_permission_profiles = effective_permissions .as_ref() .is_some_and(|profiles| !profiles.is_empty()); - let default_permissions = default_permissions_override + let allowed_permissions = requirements_toml.allowed_permissions.as_ref(); + let mut default_permissions = default_permissions_override .as_deref() + .or(requirements_toml.default_permissions.as_deref()) .or(cfg.default_permissions.as_deref()); - validate_user_permission_profile_names(cfg.permissions.as_ref())?; + if default_permissions.is_none() { + default_permissions = allowed_permissions + .and_then(|allowed_permissions| allowed_permissions.first()) + .map(String::as_str); + } + if let (Some(selected_permissions), Some(allowed_permissions)) = + (default_permissions, allowed_permissions) + && !allowed_permissions + .iter() + .any(|allowed_permission| allowed_permission == selected_permissions) + { + let Some(fallback_permissions) = requirements_toml + .default_permissions + .as_deref() + .or_else(|| allowed_permissions.first().map(String::as_str)) + else { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + "requirements.toml allowed_permissions must include at least one profile", + )); + }; + startup_warnings.push(format!( + "Configured permission profile `{selected_permissions}` is disallowed by requirements; falling back to required value `{fallback_permissions}`." + )); + default_permissions = Some(fallback_permissions); + } if has_permission_profiles && !matches!( permission_config_syntax, @@ -2663,7 +2700,7 @@ impl Config { ) }); network_proxy_config_for_profile_selection( - cfg.permissions.as_ref(), + effective_permissions.as_ref(), default_permissions, )? } else { @@ -2710,19 +2747,19 @@ impl Config { None }; let configured_network_proxy_config = network_proxy_config_for_profile_selection( - cfg.permissions.as_ref(), + effective_permissions.as_ref(), default_permissions, )?; let (mut file_system_sandbox_policy, network_sandbox_policy) = compile_permission_profile_selection( - cfg.permissions.as_ref(), + effective_permissions.as_ref(), default_permissions, builtin_workspace_write_settings, resolved_cwd.as_path(), &mut startup_warnings, )?; let mut configured_workspace_roots = compile_permission_profile_workspace_roots( - cfg.permissions.as_ref(), + effective_permissions.as_ref(), default_permissions, resolved_cwd.as_path(), )?; @@ -3669,6 +3706,96 @@ fn guardian_policy_config_from_requirements( normalize_guardian_policy_config(requirements_toml.guardian_policy_config.as_deref()) } +fn merge_managed_permission_profiles( + configured_permissions: Option<&PermissionsToml>, + requirements_toml: &ConfigRequirementsToml, +) -> std::io::Result> { + let managed_profiles = requirements_toml + .permissions + .as_ref() + .map(|permissions| &permissions.profiles) + .filter(|profiles| !profiles.is_empty()); + let Some(managed_profiles) = managed_profiles else { + return Ok(configured_permissions.cloned()); + }; + + let mut merged_permissions = configured_permissions.cloned().unwrap_or_default(); + for (profile_id, managed_profile) in managed_profiles { + if merged_permissions.entries.contains_key(profile_id) { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + format!( + "requirements.toml permissions profile `{profile_id}` conflicts with a config-defined profile of the same name" + ), + )); + } + merged_permissions + .entries + .insert(profile_id.clone(), managed_profile.clone()); + } + + Ok(Some(merged_permissions)) +} + +fn validate_required_permission_profile_catalog( + requirements_toml: &ConfigRequirementsToml, + available_permissions: Option<&PermissionsToml>, +) -> std::io::Result<()> { + let is_known_profile = |profile_id: &str| { + is_builtin_permission_profile_name(profile_id) + || available_permissions + .as_ref() + .is_some_and(|permissions| permissions.entries.contains_key(profile_id)) + }; + + if let Some(default_permissions) = requirements_toml.default_permissions.as_deref() + && !is_known_profile(default_permissions) + { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + format!( + "requirements.toml default_permissions refers to undefined profile `{default_permissions}`" + ), + )); + } + + let Some(allowed_permissions) = requirements_toml.allowed_permissions.as_ref() else { + return Ok(()); + }; + if allowed_permissions.is_empty() { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + "requirements.toml allowed_permissions must include at least one profile", + )); + } + + for profile_id in allowed_permissions { + if !is_known_profile(profile_id) { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + format!( + "requirements.toml allowed_permissions refers to undefined profile `{profile_id}`" + ), + )); + } + } + + if let Some(default_permissions) = requirements_toml.default_permissions.as_deref() + && !allowed_permissions + .iter() + .any(|profile_id| profile_id == default_permissions) + { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + format!( + "requirements.toml default_permissions `{default_permissions}` must also be listed in allowed_permissions" + ), + )); + } + + Ok(()) +} + fn normalize_guardian_policy_config(value: Option<&str>) -> Option { value.and_then(|value| { let trimmed = value.trim(); diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 7eb4539f1d09..0e4d9c5c51f6 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -697,6 +697,8 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::OnRequest.to_core()]), allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]), allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]), + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]), allow_managed_hooks_only: Some(true), @@ -911,6 +913,8 @@ approval_policy = "never" allowed_approval_policies: None, allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(Vec::new()), allow_managed_hooks_only: None, From 87f1e8c4d03ef28b9a194e7b1a30f0acb30f453a Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 18 May 2026 22:04:36 -0700 Subject: [PATCH 3/7] Revert "feat(config): support managed permission profiles in requirements" This reverts commit 05c815cae4e9d121191802b44b7cbe86eb0301e5. --- .../codex_app_server_protocol.schemas.json | 15 -- .../codex_app_server_protocol.v2.schemas.json | 15 -- .../v2/ConfigRequirementsReadResponse.json | 15 -- .../typescript/v2/ConfigRequirements.ts | 2 +- .../src/protocol/v2/config.rs | 2 - .../src/protocol/v2/tests.rs | 2 - codex-rs/app-server/README.md | 4 +- .../request_processors/catalog_processor.rs | 125 +++------------ .../request_processors/config_processor.rs | 18 --- codex-rs/cloud-requirements/src/lib.rs | 32 ---- codex-rs/config/src/config_requirements.rs | 84 ---------- .../core/src/config/config_loader_tests.rs | 64 -------- codex-rs/core/src/config/config_tests.rs | 4 - codex-rs/core/src/config/mod.rs | 143 +----------------- codex-rs/tui/src/debug_config.rs | 4 - 15 files changed, 34 insertions(+), 495 deletions(-) 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 63454cb04688..ef86c74e2d17 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 @@ -7612,15 +7612,6 @@ "null" ] }, - "allowedPermissions": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, "allowedSandboxModes": { "items": { "$ref": "#/definitions/v2/SandboxMode" @@ -7639,12 +7630,6 @@ "null" ] }, - "defaultPermissions": { - "type": [ - "string", - "null" - ] - }, "enforceResidency": { "anyOf": [ { 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 4d133964b752..cd040d50c244 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 @@ -4001,15 +4001,6 @@ "null" ] }, - "allowedPermissions": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, "allowedSandboxModes": { "items": { "$ref": "#/definitions/SandboxMode" @@ -4028,12 +4019,6 @@ "null" ] }, - "defaultPermissions": { - "type": [ - "string", - "null" - ] - }, "enforceResidency": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index c4bf18e81e0a..41bd337decb1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -77,15 +77,6 @@ "null" ] }, - "allowedPermissions": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, "allowedSandboxModes": { "items": { "$ref": "#/definitions/SandboxMode" @@ -104,12 +95,6 @@ "null" ] }, - "defaultPermissions": { - "type": [ - "string", - "null" - ] - }, "enforceResidency": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts index 2456adfcf88f..1b0625e5e16e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts @@ -6,4 +6,4 @@ import type { AskForApproval } from "./AskForApproval"; import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { SandboxMode } from "./SandboxMode"; -export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, defaultPermissions: string | null, allowedPermissions: Array | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; +export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; 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 ad956459aae9..b46515d81142 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -384,8 +384,6 @@ pub struct ConfigRequirements { #[experimental("configRequirements/read.allowedApprovalsReviewers")] pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, - pub default_permissions: Option, - pub allowed_permissions: Option>, pub allowed_web_search_modes: Option>, pub allow_managed_hooks_only: Option, pub feature_requirements: Option>, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 676a0d674413..b445fc4d01af 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -1697,8 +1697,6 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() }]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, feature_requirements: None, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index deaf664b6133..599799b79558 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -189,7 +189,7 @@ Example with notification opt-out: - `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata. - `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider. - `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. Pass `threadId` when showing feature state for an existing loaded thread so `enabled` is computed from that thread's refreshed config, including project-local config for the thread's cwd; if omitted, the server uses its default config resolution context. For non-beta flags, `displayName`/`description`/`announcement` are `null`. -- `permissionProfile/list` — list available permission profile ids with optional display `description` text, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.]` entries to be included in the current catalog view. Managed `requirements.toml` profiles are included too, and `allowed_permissions` filters the returned catalog when present. +- `permissionProfile/list` — list available permission profile ids with optional display `description` text, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.]` entries to be included in the current catalog view. - `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `memories`, `plugins`, `tool_suggest`, `tool_call_mcp_elicitation`). For each feature, precedence is: cloud requirements > --enable > config.toml > experimentalFeature/enablement/set (new) > code default. - `environment/add` — experimental; add or replace a named remote environment by `environmentId` and `execServerUrl` for later selection by `thread/start` or `turn/start`; returns `{}` and does not change the default environment. - `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). Built-in presets do not select a model; the Plan preset selects medium reasoning effort. This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly. @@ -224,7 +224,7 @@ Example with notification opt-out: - `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`), permission-profile defaults/catalogs (`defaultPermissions`, `allowedPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. +- `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`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. ### Example: Start or resume a thread diff --git a/codex-rs/app-server/src/request_processors/catalog_processor.rs b/codex-rs/app-server/src/request_processors/catalog_processor.rs index 1b043875d05c..7b958118f50c 100644 --- a/codex-rs/app-server/src/request_processors/catalog_processor.rs +++ b/codex-rs/app-server/src/request_processors/catalog_processor.rs @@ -1,7 +1,5 @@ use super::*; -use codex_config::ConfigRequirementsToml; use codex_config::config_toml::ConfigToml; -use codex_config::permissions_toml::PermissionsToml; use futures::StreamExt; #[derive(Clone)] @@ -412,9 +410,29 @@ impl CatalogRequestProcessor { .effective_config() .try_into() .map_err(|err| internal_error(format!("failed to read effective config: {err}")))?; - let profiles = permission_profile_summaries( - effective_config.permissions.as_ref(), - config.config_layer_stack.requirements_toml(), + let mut profiles = vec![ + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), + description: None, + }, + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), + description: None, + }, + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), + description: None, + }, + ]; + profiles.extend( + effective_config + .permissions + .into_iter() + .flat_map(|permissions| permissions.entries) + .map(|(id, profile)| PermissionProfileSummary { + id, + description: profile.description, + }), ); let total = profiles.len(); let effective_limit = limit.unwrap_or(total as u32).max(1) as usize; @@ -649,100 +667,3 @@ impl CatalogRequestProcessor { .map_err(|err| internal_error(format!("failed to update skill settings: {err}"))) } } - -fn permission_profile_summaries( - config_permissions: Option<&PermissionsToml>, - requirements_toml: &ConfigRequirementsToml, -) -> Vec { - let mut profiles = vec![ - PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), - description: None, - }, - PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), - description: None, - }, - PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), - description: None, - }, - ]; - profiles.extend( - config_permissions - .into_iter() - .flat_map(|permissions| permissions.entries.iter()) - .map(|(id, profile)| PermissionProfileSummary { - id: id.clone(), - description: profile.description.clone(), - }), - ); - profiles.extend( - requirements_toml - .permissions - .as_ref() - .into_iter() - .flat_map(|permissions| permissions.profiles.iter()) - .map(|(id, profile)| PermissionProfileSummary { - id: id.clone(), - description: profile.description.clone(), - }), - ); - if let Some(allowed_permissions) = requirements_toml.allowed_permissions.as_ref() { - profiles.retain(|profile| { - allowed_permissions - .iter() - .any(|allowed_profile| allowed_profile == &profile.id) - }); - } - profiles -} - -#[cfg(test)] -mod tests { - use super::PermissionProfileSummary; - use super::permission_profile_summaries; - use codex_config::ConfigRequirementsToml; - use codex_config::permissions_toml::PermissionProfileToml; - use codex_config::permissions_toml::PermissionsToml; - use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY; - use pretty_assertions::assert_eq; - use std::collections::BTreeMap; - use toml::from_str; - - #[test] - fn permission_profile_summaries_include_managed_profiles_and_apply_allowlist() { - let config_permissions = PermissionsToml { - entries: BTreeMap::from([( - "local".to_string(), - PermissionProfileToml { - description: Some("Local profile.".to_string()), - ..PermissionProfileToml::default() - }, - )]), - }; - let requirements_toml: ConfigRequirementsToml = from_str( - r#" - allowed_permissions = [":read-only", "managed-standard"] - - [permissions.managed-standard] - description = "Managed profile." - "#, - ) - .expect("requirements TOML should parse"); - - assert_eq!( - permission_profile_summaries(Some(&config_permissions), &requirements_toml), - vec![ - PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), - description: None, - }, - PermissionProfileSummary { - id: "managed-standard".to_string(), - description: Some("Managed profile.".to_string()), - }, - ] - ); - } -} 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 e43728e01aef..5054ea248d67 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -418,8 +418,6 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR .filter_map(map_sandbox_mode_requirement_to_api) .collect() }), - default_permissions: requirements.default_permissions, - allowed_permissions: requirements.allowed_permissions, allowed_web_search_modes: requirements.allowed_web_search_modes.map(|modes| { let mut normalized = modes .into_iter() @@ -623,26 +621,10 @@ mod tests { #[test] fn requirements_api_includes_allow_managed_hooks_only() { let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { - default_permissions: Some("managed-standard".to_string()), - allowed_permissions: Some(vec![ - "managed-standard".to_string(), - "managed-build".to_string(), - ]), allow_managed_hooks_only: Some(true), ..ConfigRequirementsToml::default() }); - assert_eq!( - mapped.default_permissions, - Some("managed-standard".to_string()) - ); - assert_eq!( - mapped.allowed_permissions, - Some(vec![ - "managed-standard".to_string(), - "managed-build".to_string(), - ]) - ); assert_eq!(mapped.allow_managed_hooks_only, Some(true)); assert_eq!(mapped.hooks, None); } diff --git a/codex-rs/cloud-requirements/src/lib.rs b/codex-rs/cloud-requirements/src/lib.rs index 992ab25cef05..8812762cbed2 100644 --- a/codex-rs/cloud-requirements/src/lib.rs +++ b/codex-rs/cloud-requirements/src/lib.rs @@ -1202,8 +1202,6 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1287,8 +1285,6 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1323,8 +1319,6 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1376,8 +1370,6 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1529,8 +1521,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1612,8 +1602,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1693,8 +1681,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1902,8 +1888,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1945,8 +1929,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2008,8 +1990,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2067,8 +2047,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2128,8 +2106,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2190,8 +2166,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2252,8 +2226,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2344,8 +2316,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2378,8 +2348,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 48d0ec5b9097..4e4336a2b505 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -19,7 +19,6 @@ use crate::Constrained; use crate::ConstraintError; use crate::ManagedHooksRequirementsToml; use crate::mcp_types::AppToolApproval; -use crate::permissions_toml::PermissionProfileToml; #[derive(Debug, Clone, PartialEq, Eq)] pub enum RequirementSource { @@ -439,8 +438,6 @@ pub struct FilesystemRequirementsToml { #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct PermissionsRequirementsToml { pub filesystem: Option, - #[serde(default, flatten)] - pub profiles: BTreeMap, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -691,8 +688,6 @@ pub struct ConfigRequirementsToml { pub allowed_approval_policies: Option>, pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, - pub default_permissions: Option, - pub allowed_permissions: Option>, pub remote_sandbox_config: Option>, pub allowed_web_search_modes: Option>, pub allow_managed_hooks_only: Option, @@ -743,8 +738,6 @@ pub struct ConfigRequirementsWithSources { pub allowed_approval_policies: Option>>, pub allowed_approvals_reviewers: Option>>, pub allowed_sandbox_modes: Option>>, - pub default_permissions: Option>, - pub allowed_permissions: Option>>, pub allowed_web_search_modes: Option>>, pub allow_managed_hooks_only: Option>, pub feature_requirements: Option>, @@ -781,8 +774,6 @@ impl ConfigRequirementsWithSources { allowed_approval_policies: _, allowed_approvals_reviewers: _, allowed_sandbox_modes: _, - default_permissions: _, - allowed_permissions: _, remote_sandbox_config: _, allowed_web_search_modes: _, allow_managed_hooks_only: _, @@ -814,8 +805,6 @@ impl ConfigRequirementsWithSources { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, - default_permissions, - allowed_permissions, allowed_web_search_modes, allow_managed_hooks_only, feature_requirements, @@ -844,8 +833,6 @@ impl ConfigRequirementsWithSources { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, - default_permissions, - allowed_permissions, allowed_web_search_modes, allow_managed_hooks_only, feature_requirements, @@ -863,8 +850,6 @@ impl ConfigRequirementsWithSources { allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value), allowed_approvals_reviewers: allowed_approvals_reviewers.map(|sourced| sourced.value), allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value), - default_permissions: default_permissions.map(|sourced| sourced.value), - allowed_permissions: allowed_permissions.map(|sourced| sourced.value), remote_sandbox_config: None, allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value), allow_managed_hooks_only: allow_managed_hooks_only.map(|sourced| sourced.value), @@ -949,8 +934,6 @@ impl ConfigRequirementsToml { self.allowed_approval_policies.is_none() && self.allowed_approvals_reviewers.is_none() && self.allowed_sandbox_modes.is_none() - && self.default_permissions.is_none() - && self.allowed_permissions.is_none() && self.remote_sandbox_config.is_none() && self.allowed_web_search_modes.is_none() && self.allow_managed_hooks_only.is_none() @@ -990,8 +973,6 @@ impl TryFrom for ConfigRequirements { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, - default_permissions: _default_permissions, - allowed_permissions: _allowed_permissions, allowed_web_search_modes, allow_managed_hooks_only, feature_requirements, @@ -1295,8 +1276,6 @@ mod tests { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, - default_permissions, - allowed_permissions, remote_sandbox_config: _, allowed_web_search_modes, allow_managed_hooks_only, @@ -1318,10 +1297,6 @@ mod tests { .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_sandbox_modes: allowed_sandbox_modes .map(|value| Sourced::new(value, RequirementSource::Unknown)), - default_permissions: default_permissions - .map(|value| Sourced::new(value, RequirementSource::Unknown)), - allowed_permissions: allowed_permissions - .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_web_search_modes: allowed_web_search_modes .map(|value| Sourced::new(value, RequirementSource::Unknown)), allow_managed_hooks_only: allow_managed_hooks_only @@ -1368,54 +1343,6 @@ mod tests { Ok(()) } - #[test] - fn deserialize_managed_permission_profiles() -> Result<()> { - let requirements: ConfigRequirementsToml = from_str( - r#" - default_permissions = "managed-standard" - allowed_permissions = ["managed-standard", "managed-build"] - - [permissions.managed-standard] - description = "General managed access." - - [permissions.managed-build.filesystem] - ":workspace_roots" = "write" - "#, - )?; - - assert_eq!( - requirements.default_permissions.as_deref(), - Some("managed-standard") - ); - assert_eq!( - requirements.allowed_permissions, - Some(vec![ - "managed-standard".to_string(), - "managed-build".to_string(), - ]) - ); - let permissions = requirements - .permissions - .as_ref() - .expect("managed permission profiles"); - assert_eq!( - permissions - .profiles - .get("managed-standard") - .and_then(|profile| profile.description.as_deref()), - Some("General managed access.") - ); - assert!( - permissions - .profiles - .get("managed-build") - .and_then(|profile| profile.filesystem.as_ref()) - .is_some() - ); - assert!(!requirements.is_empty()); - Ok(()) - } - #[test] fn merge_unset_fields_copies_every_field_and_sets_sources() { let mut target = ConfigRequirementsWithSources::default(); @@ -1445,8 +1372,6 @@ mod tests { allowed_approval_policies: Some(allowed_approval_policies.clone()), allowed_approvals_reviewers: Some(allowed_approvals_reviewers.clone()), allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()), - default_permissions: Some("managed".to_string()), - allowed_permissions: Some(vec!["managed".to_string()]), remote_sandbox_config: None, allowed_web_search_modes: Some(allowed_web_search_modes.clone()), allow_managed_hooks_only: Some(true), @@ -1476,11 +1401,6 @@ mod tests { source.clone(), )), allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source.clone(),)), - default_permissions: Some(Sourced::new("managed".to_string(), source.clone(),)), - allowed_permissions: Some(Sourced::new( - vec!["managed".to_string()], - source.clone(), - )), allowed_web_search_modes: Some(Sourced::new( allowed_web_search_modes, enforce_source.clone(), @@ -1530,8 +1450,6 @@ mod tests { )), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, feature_requirements: None, @@ -1580,8 +1498,6 @@ mod tests { )), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, feature_requirements: None, diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 6960dcf995b3..cd84f06cda00 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -1095,8 +1095,6 @@ allowed_approval_policies = ["on-request"] allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1155,8 +1153,6 @@ allowed_approval_policies = ["on-request"] allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1364,8 +1360,6 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()> allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1414,64 +1408,6 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()> Ok(()) } -#[tokio::test] -async fn system_requirements_define_managed_permission_profiles() -> anyhow::Result<()> { - let tmp = tempdir()?; - let codex_home = tmp.path().join("home"); - tokio::fs::create_dir_all(&codex_home).await?; - tokio::fs::write( - codex_home.join(CONFIG_TOML_FILE), - r#" -default_permissions = "local" - -[permissions.local.filesystem] -":workspace_roots" = "read" -"#, - ) - .await?; - let requirements_path = tmp.path().join("requirements.toml"); - tokio::fs::write( - &requirements_path, - r#" -default_permissions = "managed-standard" -allowed_permissions = ["managed-standard"] - -[permissions.managed-standard] -description = "Managed profile." - -[permissions.managed-standard.filesystem] -":workspace_roots" = "read" -"#, - ) - .await?; - - let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?; - let mut overrides = LoaderOverrides::without_managed_config_for_tests(); - overrides.system_requirements_path = Some(requirements_path); - let config = ConfigBuilder::default() - .codex_home(codex_home) - .fallback_cwd(Some(cwd.to_path_buf())) - .loader_overrides(overrides) - .build() - .await?; - - assert_eq!( - config - .config_layer_stack - .requirements_toml() - .allowed_permissions, - Some(vec!["managed-standard".to_string()]) - ); - assert_eq!( - config - .permissions - .active_permission_profile() - .map(|profile| profile.id), - Some("managed-standard".to_string()) - ); - Ok(()) -} - #[tokio::test] async fn load_config_layers_can_ignore_managed_requirements() -> anyhow::Result<()> { let tmp = tempdir()?; diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 36b27812ac05..8314c43b89c5 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -8632,8 +8632,6 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() allowed_approval_policies: None, allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]), allow_managed_hooks_only: None, @@ -9417,8 +9415,6 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s allowed_approval_policies: None, allowed_approvals_reviewers: None, allowed_sandbox_modes: Some(vec![codex_config::SandboxModeRequirement::ReadOnly]), - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index f92bdd50cd9e..583c2a42b6f1 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -33,7 +33,6 @@ use codex_config::config_toml::ThreadStoreToml; use codex_config::config_toml::validate_model_providers; use codex_config::loader::load_config_layers_state; use codex_config::loader::project_trust_key; -use codex_config::permissions_toml::PermissionsToml; use codex_config::profile_toml::ConfigProfile; use codex_config::sandbox_mode_requirement_for_permission_profile; use codex_config::types::ApprovalsReviewer; @@ -121,7 +120,6 @@ use crate::config::permissions::compile_permission_profile_selection; use crate::config::permissions::compile_permission_profile_workspace_roots; use crate::config::permissions::default_builtin_permission_profile_name; use crate::config::permissions::get_readable_roots_required_for_codex_runtime; -use crate::config::permissions::is_builtin_permission_profile_name; use crate::config::permissions::network_proxy_config_for_profile_selection; use crate::config::permissions::validate_user_permission_profile_names; use crate::config_lock::config_without_lock_controls; @@ -2570,49 +2568,14 @@ impl Config { sandbox_mode, config_profile.sandbox_mode, ); - let requirements_toml = config_layer_stack.requirements_toml(); - let effective_permissions = - merge_managed_permission_profiles(cfg.permissions.as_ref(), requirements_toml)?; - validate_user_permission_profile_names(effective_permissions.as_ref())?; - validate_required_permission_profile_catalog( - requirements_toml, - effective_permissions.as_ref(), - )?; - - let has_permission_profiles = effective_permissions + let has_permission_profiles = cfg + .permissions .as_ref() .is_some_and(|profiles| !profiles.is_empty()); - let allowed_permissions = requirements_toml.allowed_permissions.as_ref(); - let mut default_permissions = default_permissions_override + let default_permissions = default_permissions_override .as_deref() - .or(requirements_toml.default_permissions.as_deref()) .or(cfg.default_permissions.as_deref()); - if default_permissions.is_none() { - default_permissions = allowed_permissions - .and_then(|allowed_permissions| allowed_permissions.first()) - .map(String::as_str); - } - if let (Some(selected_permissions), Some(allowed_permissions)) = - (default_permissions, allowed_permissions) - && !allowed_permissions - .iter() - .any(|allowed_permission| allowed_permission == selected_permissions) - { - let Some(fallback_permissions) = requirements_toml - .default_permissions - .as_deref() - .or_else(|| allowed_permissions.first().map(String::as_str)) - else { - return Err(std::io::Error::new( - ErrorKind::InvalidInput, - "requirements.toml allowed_permissions must include at least one profile", - )); - }; - startup_warnings.push(format!( - "Configured permission profile `{selected_permissions}` is disallowed by requirements; falling back to required value `{fallback_permissions}`." - )); - default_permissions = Some(fallback_permissions); - } + validate_user_permission_profile_names(cfg.permissions.as_ref())?; if has_permission_profiles && !matches!( permission_config_syntax, @@ -2700,7 +2663,7 @@ impl Config { ) }); network_proxy_config_for_profile_selection( - effective_permissions.as_ref(), + cfg.permissions.as_ref(), default_permissions, )? } else { @@ -2747,19 +2710,19 @@ impl Config { None }; let configured_network_proxy_config = network_proxy_config_for_profile_selection( - effective_permissions.as_ref(), + cfg.permissions.as_ref(), default_permissions, )?; let (mut file_system_sandbox_policy, network_sandbox_policy) = compile_permission_profile_selection( - effective_permissions.as_ref(), + cfg.permissions.as_ref(), default_permissions, builtin_workspace_write_settings, resolved_cwd.as_path(), &mut startup_warnings, )?; let mut configured_workspace_roots = compile_permission_profile_workspace_roots( - effective_permissions.as_ref(), + cfg.permissions.as_ref(), default_permissions, resolved_cwd.as_path(), )?; @@ -3706,96 +3669,6 @@ fn guardian_policy_config_from_requirements( normalize_guardian_policy_config(requirements_toml.guardian_policy_config.as_deref()) } -fn merge_managed_permission_profiles( - configured_permissions: Option<&PermissionsToml>, - requirements_toml: &ConfigRequirementsToml, -) -> std::io::Result> { - let managed_profiles = requirements_toml - .permissions - .as_ref() - .map(|permissions| &permissions.profiles) - .filter(|profiles| !profiles.is_empty()); - let Some(managed_profiles) = managed_profiles else { - return Ok(configured_permissions.cloned()); - }; - - let mut merged_permissions = configured_permissions.cloned().unwrap_or_default(); - for (profile_id, managed_profile) in managed_profiles { - if merged_permissions.entries.contains_key(profile_id) { - return Err(std::io::Error::new( - ErrorKind::InvalidInput, - format!( - "requirements.toml permissions profile `{profile_id}` conflicts with a config-defined profile of the same name" - ), - )); - } - merged_permissions - .entries - .insert(profile_id.clone(), managed_profile.clone()); - } - - Ok(Some(merged_permissions)) -} - -fn validate_required_permission_profile_catalog( - requirements_toml: &ConfigRequirementsToml, - available_permissions: Option<&PermissionsToml>, -) -> std::io::Result<()> { - let is_known_profile = |profile_id: &str| { - is_builtin_permission_profile_name(profile_id) - || available_permissions - .as_ref() - .is_some_and(|permissions| permissions.entries.contains_key(profile_id)) - }; - - if let Some(default_permissions) = requirements_toml.default_permissions.as_deref() - && !is_known_profile(default_permissions) - { - return Err(std::io::Error::new( - ErrorKind::InvalidInput, - format!( - "requirements.toml default_permissions refers to undefined profile `{default_permissions}`" - ), - )); - } - - let Some(allowed_permissions) = requirements_toml.allowed_permissions.as_ref() else { - return Ok(()); - }; - if allowed_permissions.is_empty() { - return Err(std::io::Error::new( - ErrorKind::InvalidInput, - "requirements.toml allowed_permissions must include at least one profile", - )); - } - - for profile_id in allowed_permissions { - if !is_known_profile(profile_id) { - return Err(std::io::Error::new( - ErrorKind::InvalidInput, - format!( - "requirements.toml allowed_permissions refers to undefined profile `{profile_id}`" - ), - )); - } - } - - if let Some(default_permissions) = requirements_toml.default_permissions.as_deref() - && !allowed_permissions - .iter() - .any(|profile_id| profile_id == default_permissions) - { - return Err(std::io::Error::new( - ErrorKind::InvalidInput, - format!( - "requirements.toml default_permissions `{default_permissions}` must also be listed in allowed_permissions" - ), - )); - } - - Ok(()) -} - fn normalize_guardian_policy_config(value: Option<&str>) -> Option { value.and_then(|value| { let trimmed = value.trim(); diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 0e4d9c5c51f6..7eb4539f1d09 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -697,8 +697,6 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::OnRequest.to_core()]), allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]), allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]), - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]), allow_managed_hooks_only: Some(true), @@ -913,8 +911,6 @@ approval_policy = "never" allowed_approval_policies: None, allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, - allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(Vec::new()), allow_managed_hooks_only: None, From 001506002518823ecfc040797ed7d71522e4c7e1 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 18 May 2026 22:34:13 -0700 Subject: [PATCH 4/7] fix(app-server): gate permission profile list API Co-authored-by: Codex noreply@openai.com --- .../schema/json/ClientRequest.json | 52 ------------- .../codex_app_server_protocol.schemas.json | 77 ------------------- .../codex_app_server_protocol.v2.schemas.json | 77 ------------------- .../json/v2/PermissionProfileListParams.json | 30 -------- .../v2/PermissionProfileListResponse.json | 44 ----------- .../schema/typescript/ClientRequest.ts | 3 +- .../v2/PermissionProfileListParams.ts | 17 ---- .../v2/PermissionProfileListResponse.ts | 11 --- .../schema/typescript/v2/index.ts | 2 - .../src/protocol/common.rs | 11 +++ .../request_processors/catalog_processor.rs | 19 ++++- .../tests/suite/v2/permission_profile_list.rs | 58 ++++++++++++++ 12 files changed, 86 insertions(+), 315 deletions(-) delete mode 100644 codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListParams.json delete mode 100644 codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListResponse.json delete mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListParams.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListResponse.ts diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 1df52717f2cc..2621a0bd1eac 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1541,34 +1541,6 @@ ], "type": "string" }, - "PermissionProfileListParams": { - "properties": { - "cursor": { - "description": "Opaque pagination cursor returned by a previous call.", - "type": [ - "string", - "null" - ] - }, - "cwd": { - "description": "Optional working directory to resolve project config layers.", - "type": [ - "string", - "null" - ] - }, - "limit": { - "description": "Optional page size; defaults to the full result set.", - "format": "uint32", - "minimum": 0.0, - "type": [ - "integer", - "null" - ] - } - }, - "type": "object" - }, "Personality": { "enum": [ "none", @@ -5352,30 +5324,6 @@ "title": "ExperimentalFeature/listRequest", "type": "object" }, - { - "properties": { - "id": { - "$ref": "#/definitions/RequestId" - }, - "method": { - "enum": [ - "permissionProfile/list" - ], - "title": "PermissionProfile/listRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/PermissionProfileListParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "PermissionProfile/listRequest", - "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 ef86c74e2d17..e03ca73b6e79 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 @@ -1429,30 +1429,6 @@ "title": "ExperimentalFeature/listRequest", "type": "object" }, - { - "properties": { - "id": { - "$ref": "#/definitions/v2/RequestId" - }, - "method": { - "enum": [ - "permissionProfile/list" - ], - "title": "PermissionProfile/listRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/v2/PermissionProfileListParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "PermissionProfile/listRequest", - "type": "object" - }, { "properties": { "id": { @@ -11674,59 +11650,6 @@ } ] }, - "PermissionProfileListParams": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "cursor": { - "description": "Opaque pagination cursor returned by a previous call.", - "type": [ - "string", - "null" - ] - }, - "cwd": { - "description": "Optional working directory to resolve project config layers.", - "type": [ - "string", - "null" - ] - }, - "limit": { - "description": "Optional page size; defaults to the full result set.", - "format": "uint32", - "minimum": 0.0, - "type": [ - "integer", - "null" - ] - } - }, - "title": "PermissionProfileListParams", - "type": "object" - }, - "PermissionProfileListResponse": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "data": { - "items": { - "$ref": "#/definitions/v2/PermissionProfileSummary" - }, - "type": "array" - }, - "nextCursor": { - "description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "data" - ], - "title": "PermissionProfileListResponse", - "type": "object" - }, "PermissionProfileSummary": { "properties": { "description": { 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 cd040d50c244..e8cab7d796cc 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 @@ -2136,30 +2136,6 @@ "title": "ExperimentalFeature/listRequest", "type": "object" }, - { - "properties": { - "id": { - "$ref": "#/definitions/RequestId" - }, - "method": { - "enum": [ - "permissionProfile/list" - ], - "title": "PermissionProfile/listRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/PermissionProfileListParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "PermissionProfile/listRequest", - "type": "object" - }, { "properties": { "id": { @@ -8223,59 +8199,6 @@ } ] }, - "PermissionProfileListParams": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "cursor": { - "description": "Opaque pagination cursor returned by a previous call.", - "type": [ - "string", - "null" - ] - }, - "cwd": { - "description": "Optional working directory to resolve project config layers.", - "type": [ - "string", - "null" - ] - }, - "limit": { - "description": "Optional page size; defaults to the full result set.", - "format": "uint32", - "minimum": 0.0, - "type": [ - "integer", - "null" - ] - } - }, - "title": "PermissionProfileListParams", - "type": "object" - }, - "PermissionProfileListResponse": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "data": { - "items": { - "$ref": "#/definitions/PermissionProfileSummary" - }, - "type": "array" - }, - "nextCursor": { - "description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "data" - ], - "title": "PermissionProfileListResponse", - "type": "object" - }, "PermissionProfileSummary": { "properties": { "description": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListParams.json b/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListParams.json deleted file mode 100644 index 402dab622168..000000000000 --- a/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListParams.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "cursor": { - "description": "Opaque pagination cursor returned by a previous call.", - "type": [ - "string", - "null" - ] - }, - "cwd": { - "description": "Optional working directory to resolve project config layers.", - "type": [ - "string", - "null" - ] - }, - "limit": { - "description": "Optional page size; defaults to the full result set.", - "format": "uint32", - "minimum": 0.0, - "type": [ - "integer", - "null" - ] - } - }, - "title": "PermissionProfileListParams", - "type": "object" -} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListResponse.json deleted file mode 100644 index 4d5a47f8d5d6..000000000000 --- a/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListResponse.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "PermissionProfileSummary": { - "properties": { - "description": { - "description": "Optional user-facing description for display in clients.", - "type": [ - "string", - "null" - ] - }, - "id": { - "description": "Available permission profile identifier.", - "type": "string" - } - }, - "required": [ - "id" - ], - "type": "object" - } - }, - "properties": { - "data": { - "items": { - "$ref": "#/definitions/PermissionProfileSummary" - }, - "type": "array" - }, - "nextCursor": { - "description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "data" - ], - "title": "PermissionProfileListResponse", - "type": "object" -} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 14be7db47aa4..7f017ed70089 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -42,7 +42,6 @@ import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams"; import type { McpServerToolCallParams } from "./v2/McpServerToolCallParams"; import type { ModelListParams } from "./v2/ModelListParams"; import type { ModelProviderCapabilitiesReadParams } from "./v2/ModelProviderCapabilitiesReadParams"; -import type { PermissionProfileListParams } from "./v2/PermissionProfileListParams"; import type { PluginInstallParams } from "./v2/PluginInstallParams"; import type { PluginInstalledParams } from "./v2/PluginInstalledParams"; import type { PluginListParams } from "./v2/PluginListParams"; @@ -82,4 +81,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/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/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": "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, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListParams.ts deleted file mode 100644 index 24582c9507ad..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListParams.ts +++ /dev/null @@ -1,17 +0,0 @@ -// 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 PermissionProfileListParams = { -/** - * Opaque pagination cursor returned by a previous call. - */ -cursor?: string | null, -/** - * Optional page size; defaults to the full result set. - */ -limit?: number | null, -/** - * Optional working directory to resolve project config layers. - */ -cwd?: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListResponse.ts deleted file mode 100644 index ba0ccbc0d60f..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// 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 { PermissionProfileSummary } from "./PermissionProfileSummary"; - -export type PermissionProfileListResponse = { data: Array, -/** - * Opaque cursor to pass to the next call to continue after the last item. - * If None, there are no more items to return. - */ -nextCursor: string | null, }; 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 deed91a4bca5..2b0bd8483b8d 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -255,8 +255,6 @@ export type { OverriddenMetadata } from "./OverriddenMetadata"; export type { PatchApplyStatus } from "./PatchApplyStatus"; export type { PatchChangeKind } from "./PatchChangeKind"; export type { PermissionGrantScope } from "./PermissionGrantScope"; -export type { PermissionProfileListParams } from "./PermissionProfileListParams"; -export type { PermissionProfileListResponse } from "./PermissionProfileListResponse"; export type { PermissionProfileSummary } from "./PermissionProfileSummary"; export type { PermissionsRequestApprovalParams } from "./PermissionsRequestApprovalParams"; export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 4adcf5f5927f..6746f8324ff6 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -800,6 +800,7 @@ client_request_definitions! { serialization: global("config"), response: v2::ExperimentalFeatureListResponse, }, + #[experimental("permissionProfile/list")] PermissionProfileList => "permissionProfile/list" { params: v2::PermissionProfileListParams, serialization: global_shared_read("config"), @@ -2982,6 +2983,16 @@ mod tests { assert_eq!(reason, Some("environment/add")); } + #[test] + fn permission_profile_list_is_marked_experimental() { + let request = ClientRequest::PermissionProfileList { + request_id: RequestId::Integer(1), + params: v2::PermissionProfileListParams::default(), + }; + let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request); + assert_eq!(reason, Some("permissionProfile/list")); + } + #[test] fn command_exec_permission_profile_is_marked_experimental() { let request = ClientRequest::OneOffCommandExec { diff --git a/codex-rs/app-server/src/request_processors/catalog_processor.rs b/codex-rs/app-server/src/request_processors/catalog_processor.rs index 7b958118f50c..043c92a560cd 100644 --- a/codex-rs/app-server/src/request_processors/catalog_processor.rs +++ b/codex-rs/app-server/src/request_processors/catalog_processor.rs @@ -404,9 +404,22 @@ impl CatalogRequestProcessor { params: PermissionProfileListParams, ) -> Result { let PermissionProfileListParams { cursor, limit, cwd } = params; - let config = self.load_latest_config(cwd.map(PathBuf::from)).await?; - let effective_config: ConfigToml = config - .config_layer_stack + let config_layer_stack = match cwd { + Some(cwd) => { + let cwd = PathBuf::from(cwd); + let (_, config_layer_stack) = self + .resolve_cwd_config(&cwd) + .await + .map_err(|err| internal_error(format!("failed to reload config: {err}")))?; + config_layer_stack + } + None => self + .config_manager + .load_config_layers(/*cwd*/ None) + .await + .map_err(|err| internal_error(format!("failed to reload config: {err}")))?, + }; + let effective_config: ConfigToml = config_layer_stack .effective_config() .try_into() .map_err(|err| internal_error(format!("failed to read effective config: {err}")))?; diff --git a/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs b/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs index 8d22c8b85d06..593906528016 100644 --- a/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs +++ b/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs @@ -162,6 +162,64 @@ description = "Project-scoped profile." Ok(()) } +#[tokio::test] +async fn permission_profile_list_discovers_project_profiles_without_default_selection() -> Result<()> +{ + let codex_home = TempDir::new()?; + let workspace = TempDir::new()?; + let project_config_dir = workspace.path().join(".codex"); + std::fs::create_dir_all(&project_config_dir)?; + std::fs::write( + project_config_dir.join("config.toml"), + r#" +[permissions.project] +description = "Project-scoped profile." + +[permissions.project.filesystem] +":workspace_roots" = "write" +"#, + )?; + set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_permission_profile_list_request(PermissionProfileListParams { + cursor: None, + limit: None, + cwd: Some(workspace.path().to_string_lossy().into_owned()), + }) + .await?; + let actual = read_response::(&mut mcp, request_id).await?; + + assert_eq!( + actual, + PermissionProfileListResponse { + data: vec![ + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), + description: None, + }, + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), + description: None, + }, + PermissionProfileSummary { + id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), + description: None, + }, + PermissionProfileSummary { + id: "project".to_string(), + description: Some("Project-scoped profile.".to_string()), + }, + ], + next_cursor: None, + } + ); + Ok(()) +} + async fn read_response( mcp: &mut McpProcess, request_id: i64, From c171add5b34526f8655194cd0ba975adc138435d Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 18 May 2026 22:45:23 -0700 Subject: [PATCH 5/7] fix(app-server): expose permission profile list as beta Co-authored-by: Codex noreply@openai.com --- .../schema/json/ClientRequest.json | 52 +++++++++++++ .../codex_app_server_protocol.schemas.json | 77 +++++++++++++++++++ .../codex_app_server_protocol.v2.schemas.json | 77 +++++++++++++++++++ .../json/v2/PermissionProfileListParams.json | 30 ++++++++ .../v2/PermissionProfileListResponse.json | 44 +++++++++++ .../schema/typescript/ClientRequest.ts | 3 +- .../v2/PermissionProfileListParams.ts | 17 ++++ .../v2/PermissionProfileListResponse.ts | 11 +++ .../schema/typescript/v2/index.ts | 2 + .../src/protocol/common.rs | 11 --- codex-rs/app-server/README.md | 2 +- 11 files changed, 313 insertions(+), 13 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListResponse.json create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListResponse.ts diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 2621a0bd1eac..1df52717f2cc 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1541,6 +1541,34 @@ ], "type": "string" }, + "PermissionProfileListParams": { + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "cwd": { + "description": "Optional working directory to resolve project config layers.", + "type": [ + "string", + "null" + ] + }, + "limit": { + "description": "Optional page size; defaults to the full result set.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + }, "Personality": { "enum": [ "none", @@ -5324,6 +5352,30 @@ "title": "ExperimentalFeature/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "permissionProfile/list" + ], + "title": "PermissionProfile/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PermissionProfileListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "PermissionProfile/listRequest", + "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 e03ca73b6e79..ef86c74e2d17 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 @@ -1429,6 +1429,30 @@ "title": "ExperimentalFeature/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "permissionProfile/list" + ], + "title": "PermissionProfile/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/PermissionProfileListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "PermissionProfile/listRequest", + "type": "object" + }, { "properties": { "id": { @@ -11650,6 +11674,59 @@ } ] }, + "PermissionProfileListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "cwd": { + "description": "Optional working directory to resolve project config layers.", + "type": [ + "string", + "null" + ] + }, + "limit": { + "description": "Optional page size; defaults to the full result set.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "title": "PermissionProfileListParams", + "type": "object" + }, + "PermissionProfileListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/v2/PermissionProfileSummary" + }, + "type": "array" + }, + "nextCursor": { + "description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "PermissionProfileListResponse", + "type": "object" + }, "PermissionProfileSummary": { "properties": { "description": { 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 e8cab7d796cc..cd040d50c244 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 @@ -2136,6 +2136,30 @@ "title": "ExperimentalFeature/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "permissionProfile/list" + ], + "title": "PermissionProfile/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PermissionProfileListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "PermissionProfile/listRequest", + "type": "object" + }, { "properties": { "id": { @@ -8199,6 +8223,59 @@ } ] }, + "PermissionProfileListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "cwd": { + "description": "Optional working directory to resolve project config layers.", + "type": [ + "string", + "null" + ] + }, + "limit": { + "description": "Optional page size; defaults to the full result set.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "title": "PermissionProfileListParams", + "type": "object" + }, + "PermissionProfileListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/PermissionProfileSummary" + }, + "type": "array" + }, + "nextCursor": { + "description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "PermissionProfileListResponse", + "type": "object" + }, "PermissionProfileSummary": { "properties": { "description": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListParams.json b/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListParams.json new file mode 100644 index 000000000000..402dab622168 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListParams.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "cwd": { + "description": "Optional working directory to resolve project config layers.", + "type": [ + "string", + "null" + ] + }, + "limit": { + "description": "Optional page size; defaults to the full result set.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "title": "PermissionProfileListParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListResponse.json new file mode 100644 index 000000000000..4d5a47f8d5d6 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/PermissionProfileListResponse.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "PermissionProfileSummary": { + "properties": { + "description": { + "description": "Optional user-facing description for display in clients.", + "type": [ + "string", + "null" + ] + }, + "id": { + "description": "Available permission profile identifier.", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "properties": { + "data": { + "items": { + "$ref": "#/definitions/PermissionProfileSummary" + }, + "type": "array" + }, + "nextCursor": { + "description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "PermissionProfileListResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 7f017ed70089..14be7db47aa4 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -42,6 +42,7 @@ import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams"; import type { McpServerToolCallParams } from "./v2/McpServerToolCallParams"; import type { ModelListParams } from "./v2/ModelListParams"; import type { ModelProviderCapabilitiesReadParams } from "./v2/ModelProviderCapabilitiesReadParams"; +import type { PermissionProfileListParams } from "./v2/PermissionProfileListParams"; import type { PluginInstallParams } from "./v2/PluginInstallParams"; import type { PluginInstalledParams } from "./v2/PluginInstalledParams"; import type { PluginListParams } from "./v2/PluginListParams"; @@ -81,4 +82,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/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": "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/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, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListParams.ts new file mode 100644 index 000000000000..24582c9507ad --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListParams.ts @@ -0,0 +1,17 @@ +// 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 PermissionProfileListParams = { +/** + * Opaque pagination cursor returned by a previous call. + */ +cursor?: string | null, +/** + * Optional page size; defaults to the full result set. + */ +limit?: number | null, +/** + * Optional working directory to resolve project config layers. + */ +cwd?: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListResponse.ts new file mode 100644 index 000000000000..ba0ccbc0d60f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileListResponse.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 { PermissionProfileSummary } from "./PermissionProfileSummary"; + +export type PermissionProfileListResponse = { data: Array, +/** + * Opaque cursor to pass to the next call to continue after the last item. + * If None, there are no more items to return. + */ +nextCursor: string | null, }; 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 2b0bd8483b8d..deed91a4bca5 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -255,6 +255,8 @@ export type { OverriddenMetadata } from "./OverriddenMetadata"; export type { PatchApplyStatus } from "./PatchApplyStatus"; export type { PatchChangeKind } from "./PatchChangeKind"; export type { PermissionGrantScope } from "./PermissionGrantScope"; +export type { PermissionProfileListParams } from "./PermissionProfileListParams"; +export type { PermissionProfileListResponse } from "./PermissionProfileListResponse"; export type { PermissionProfileSummary } from "./PermissionProfileSummary"; export type { PermissionsRequestApprovalParams } from "./PermissionsRequestApprovalParams"; export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 6746f8324ff6..4adcf5f5927f 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -800,7 +800,6 @@ client_request_definitions! { serialization: global("config"), response: v2::ExperimentalFeatureListResponse, }, - #[experimental("permissionProfile/list")] PermissionProfileList => "permissionProfile/list" { params: v2::PermissionProfileListParams, serialization: global_shared_read("config"), @@ -2983,16 +2982,6 @@ mod tests { assert_eq!(reason, Some("environment/add")); } - #[test] - fn permission_profile_list_is_marked_experimental() { - let request = ClientRequest::PermissionProfileList { - request_id: RequestId::Integer(1), - params: v2::PermissionProfileListParams::default(), - }; - let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request); - assert_eq!(reason, Some("permissionProfile/list")); - } - #[test] fn command_exec_permission_profile_is_marked_experimental() { let request = ClientRequest::OneOffCommandExec { diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 599799b79558..cf71225df621 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -189,7 +189,7 @@ Example with notification opt-out: - `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata. - `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider. - `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. Pass `threadId` when showing feature state for an existing loaded thread so `enabled` is computed from that thread's refreshed config, including project-local config for the thread's cwd; if omitted, the server uses its default config resolution context. For non-beta flags, `displayName`/`description`/`announcement` are `null`. -- `permissionProfile/list` — list available permission profile ids with optional display `description` text, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.]` entries to be included in the current catalog view. +- `permissionProfile/list` — beta; list available permission profile ids with optional display `description` text, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.]` entries to be included in the current catalog view. - `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `memories`, `plugins`, `tool_suggest`, `tool_call_mcp_elicitation`). For each feature, precedence is: cloud requirements > --enable > config.toml > experimentalFeature/enablement/set (new) > code default. - `environment/add` — experimental; add or replace a named remote environment by `environmentId` and `execServerUrl` for later selection by `thread/start` or `turn/start`; returns `{}` and does not change the default environment. - `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). Built-in presets do not select a model; the Plan preset selects medium reasoning effort. This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly. From 1868afaadaf57b3302acec2a1a6af1db83bbd174 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 19 May 2026 13:10:35 -0700 Subject: [PATCH 6/7] fix(app-server): sort permission profile list results Co-authored-by: Codex noreply@openai.com --- .../request_processors/catalog_processor.rs | 1 + .../tests/suite/v2/permission_profile_list.rs | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/codex-rs/app-server/src/request_processors/catalog_processor.rs b/codex-rs/app-server/src/request_processors/catalog_processor.rs index 043c92a560cd..8e376b987f16 100644 --- a/codex-rs/app-server/src/request_processors/catalog_processor.rs +++ b/codex-rs/app-server/src/request_processors/catalog_processor.rs @@ -447,6 +447,7 @@ impl CatalogRequestProcessor { description: profile.description, }), ); + profiles.sort_by(|left, right| left.id.cmp(&right.id)); let total = profiles.len(); let effective_limit = limit.unwrap_or(total as u32).max(1) as usize; let effective_limit = effective_limit.min(total); diff --git a/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs b/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs index 593906528016..e58e628c52ef 100644 --- a/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs +++ b/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs @@ -58,15 +58,15 @@ description = "Inspect without writes." PermissionProfileListResponse { data: vec![ PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), description: None, }, PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), description: None, }, PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), description: None, }, PermissionProfileSummary { @@ -124,15 +124,15 @@ description = "Project-scoped profile." PermissionProfileListResponse { data: vec![ PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), description: None, }, PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), description: None, }, PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), description: None, }, ], @@ -198,15 +198,15 @@ description = "Project-scoped profile." PermissionProfileListResponse { data: vec![ PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), description: None, }, PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), description: None, }, PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), description: None, }, PermissionProfileSummary { From 3cf1ade71ab2c42d2fda007e72578462619f5985 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 19 May 2026 19:29:33 -0700 Subject: [PATCH 7/7] fix(app-server): preserve builtin permission profile order Co-authored-by: Codex noreply@openai.com --- .../request_processors/catalog_processor.rs | 22 +++++++++---------- .../tests/suite/v2/permission_profile_list.rs | 18 +++++++-------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/codex-rs/app-server/src/request_processors/catalog_processor.rs b/codex-rs/app-server/src/request_processors/catalog_processor.rs index 8e376b987f16..d2845d7d6ae1 100644 --- a/codex-rs/app-server/src/request_processors/catalog_processor.rs +++ b/codex-rs/app-server/src/request_processors/catalog_processor.rs @@ -437,17 +437,17 @@ impl CatalogRequestProcessor { description: None, }, ]; - profiles.extend( - effective_config - .permissions - .into_iter() - .flat_map(|permissions| permissions.entries) - .map(|(id, profile)| PermissionProfileSummary { - id, - description: profile.description, - }), - ); - profiles.sort_by(|left, right| left.id.cmp(&right.id)); + let mut configured_profiles = effective_config + .permissions + .into_iter() + .flat_map(|permissions| permissions.entries) + .map(|(id, profile)| PermissionProfileSummary { + id, + description: profile.description, + }) + .collect::>(); + configured_profiles.sort_by(|left, right| left.id.cmp(&right.id)); + profiles.extend(configured_profiles); let total = profiles.len(); let effective_limit = limit.unwrap_or(total as u32).max(1) as usize; let effective_limit = effective_limit.min(total); diff --git a/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs b/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs index e58e628c52ef..593906528016 100644 --- a/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs +++ b/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs @@ -58,15 +58,15 @@ description = "Inspect without writes." PermissionProfileListResponse { data: vec![ PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), description: None, }, PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), description: None, }, PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), description: None, }, PermissionProfileSummary { @@ -124,15 +124,15 @@ description = "Project-scoped profile." PermissionProfileListResponse { data: vec![ PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), description: None, }, PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), description: None, }, PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), description: None, }, ], @@ -198,15 +198,15 @@ description = "Project-scoped profile." PermissionProfileListResponse { data: vec![ PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), description: None, }, PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), description: None, }, PermissionProfileSummary { - id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), + id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), description: None, }, PermissionProfileSummary {