From fd25dedef60478f034f4e7f87c4ad643a06e8606 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 21 Apr 2026 17:09:36 -0700 Subject: [PATCH] app-server: accept permission profile overrides --- .../schema/json/ClientRequest.json | 313 ++++++++++++++++++ .../codex_app_server_protocol.schemas.json | 44 +++ .../codex_app_server_protocol.v2.schemas.json | 44 +++ .../schema/json/v2/ThreadForkParams.json | 284 ++++++++++++++++ .../schema/json/v2/ThreadResumeParams.json | 284 ++++++++++++++++ .../schema/json/v2/ThreadStartParams.json | 284 ++++++++++++++++ .../schema/json/v2/TurnStartParams.json | 280 ++++++++++++++++ .../schema/typescript/v2/ThreadForkParams.ts | 7 +- .../typescript/v2/ThreadResumeParams.ts | 7 +- .../schema/typescript/v2/ThreadStartParams.ts | 7 +- .../schema/typescript/v2/TurnStartParams.ts | 5 + .../app-server-protocol/src/protocol/v2.rs | 31 ++ codex-rs/app-server/README.md | 14 +- .../app-server/src/codex_message_processor.rs | 55 +++ .../src/message_processor/tracing_tests.rs | 1 + .../app-server/tests/suite/v2/skills_list.rs | 1 + .../app-server/tests/suite/v2/turn_start.rs | 2 + codex-rs/core/src/config/config_tests.rs | 42 +++ codex-rs/core/src/config/mod.rs | 38 ++- codex-rs/core/src/session/handlers.rs | 3 + codex-rs/core/src/session/session.rs | 31 +- codex-rs/core/src/session/tests.rs | 2 + .../tests/suite/collaboration_instructions.rs | 13 + codex-rs/core/tests/suite/compact.rs | 1 + codex-rs/core/tests/suite/compact_remote.rs | 2 + .../core/tests/suite/compact_resume_fork.rs | 1 + codex-rs/core/tests/suite/model_overrides.rs | 2 + codex-rs/core/tests/suite/model_switching.rs | 3 + .../core/tests/suite/model_visible_layout.rs | 1 + codex-rs/core/tests/suite/override_updates.rs | 3 + .../core/tests/suite/permissions_messages.rs | 4 + codex-rs/core/tests/suite/personality.rs | 4 + codex-rs/core/tests/suite/prompt_caching.rs | 2 + codex-rs/core/tests/suite/remote_models.rs | 2 + codex-rs/core/tests/suite/resume.rs | 1 + codex-rs/core/tests/suite/review.rs | 1 + codex-rs/exec/src/lib.rs | 2 + codex-rs/exec/src/lib_tests.rs | 12 + codex-rs/protocol/src/models.rs | 7 + codex-rs/protocol/src/protocol.rs | 5 + codex-rs/tui/src/app/tests.rs | 6 + codex-rs/tui/src/app_command.rs | 2 + codex-rs/tui/src/app_server_session.rs | 7 + .../tui/src/chatwidget/tests/permissions.rs | 1 + 44 files changed, 1844 insertions(+), 17 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 352cf02113..a78391dbec 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -12,6 +12,17 @@ ], "type": "string" }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, "ApprovalsReviewer": { "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `guardian_subagent` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request.", "enum": [ @@ -897,6 +908,217 @@ ], "type": "object" }, + "FileSystemAccessMode": { + "enum": [ + "read", + "write", + "none" + ], + "type": "string" + }, + "FileSystemPath": { + "oneOf": [ + { + "properties": { + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "enum": [ + "path" + ], + "title": "PathFileSystemPathType", + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "title": "PathFileSystemPath", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": "string" + }, + "type": { + "enum": [ + "glob_pattern" + ], + "title": "GlobPatternFileSystemPathType", + "type": "string" + } + }, + "required": [ + "pattern", + "type" + ], + "title": "GlobPatternFileSystemPath", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "special" + ], + "title": "SpecialFileSystemPathType", + "type": "string" + }, + "value": { + "$ref": "#/definitions/FileSystemSpecialPath" + } + }, + "required": [ + "type", + "value" + ], + "title": "SpecialFileSystemPath", + "type": "object" + } + ] + }, + "FileSystemSandboxEntry": { + "properties": { + "access": { + "$ref": "#/definitions/FileSystemAccessMode" + }, + "path": { + "$ref": "#/definitions/FileSystemPath" + } + }, + "required": [ + "access", + "path" + ], + "type": "object" + }, + "FileSystemSpecialPath": { + "oneOf": [ + { + "properties": { + "kind": { + "enum": [ + "root" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "RootFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "minimal" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "MinimalFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "current_working_directory" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "CurrentWorkingDirectoryFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "project_roots" + ], + "type": "string" + }, + "subpath": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "kind" + ], + "title": "KindFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "tmpdir" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "TmpdirFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "slash_tmp" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "SlashTmpFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "unknown" + ], + "type": "string" + }, + "path": { + "type": "string" + }, + "subpath": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "kind", + "path" + ], + "type": "object" + } + ] + }, "FsCopyParams": { "description": "Copy a file or directory tree on the host filesystem.", "properties": { @@ -1657,6 +1879,53 @@ ], "type": "string" }, + "PermissionProfile": { + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfileFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalNetworkPermissions" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "PermissionProfileFileSystemPermissions": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "entries" + ], + "type": "object" + }, "Personality": { "enum": [ "none", @@ -3029,6 +3298,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for the forked thread. Cannot be combined with `sandbox`." + }, "sandbox": { "anyOf": [ { @@ -3400,6 +3680,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for the resumed thread. Cannot be combined with `sandbox`." + }, "personality": { "anyOf": [ { @@ -3583,6 +3874,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for this thread. Cannot be combined with `sandbox`." + }, "personality": { "anyOf": [ { @@ -3779,6 +4081,17 @@ "outputSchema": { "description": "Optional JSON Schema used to constrain the final assistant message for this turn." }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`." + }, "personality": { "anyOf": [ { 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 f3dc8f25f1..57481eed1e 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 @@ -14027,6 +14027,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/v2/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for the forked thread. Cannot be combined with `sandbox`." + }, "sandbox": { "anyOf": [ { @@ -15411,6 +15422,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/v2/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for the resumed thread. Cannot be combined with `sandbox`." + }, "personality": { "anyOf": [ { @@ -15704,6 +15726,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/v2/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for this thread. Cannot be combined with `sandbox`." + }, "personality": { "anyOf": [ { @@ -16481,6 +16514,17 @@ "outputSchema": { "description": "Optional JSON Schema used to constrain the final assistant message for this turn." }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/v2/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`." + }, "personality": { "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 ab07d7700d..35f2dcbe65 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 @@ -11921,6 +11921,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for the forked thread. Cannot be combined with `sandbox`." + }, "sandbox": { "anyOf": [ { @@ -13305,6 +13316,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for the resumed thread. Cannot be combined with `sandbox`." + }, "personality": { "anyOf": [ { @@ -13598,6 +13620,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for this thread. Cannot be combined with `sandbox`." + }, "personality": { "anyOf": [ { @@ -14375,6 +14408,17 @@ "outputSchema": { "description": "Optional JSON Schema used to constrain the final assistant message for this turn." }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`." + }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json index a8fa95e2e9..38201f3a3d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json @@ -1,6 +1,21 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, "ApprovalsReviewer": { "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `guardian_subagent` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request.", "enum": [ @@ -59,6 +74,264 @@ } ] }, + "FileSystemAccessMode": { + "enum": [ + "read", + "write", + "none" + ], + "type": "string" + }, + "FileSystemPath": { + "oneOf": [ + { + "properties": { + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "enum": [ + "path" + ], + "title": "PathFileSystemPathType", + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "title": "PathFileSystemPath", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": "string" + }, + "type": { + "enum": [ + "glob_pattern" + ], + "title": "GlobPatternFileSystemPathType", + "type": "string" + } + }, + "required": [ + "pattern", + "type" + ], + "title": "GlobPatternFileSystemPath", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "special" + ], + "title": "SpecialFileSystemPathType", + "type": "string" + }, + "value": { + "$ref": "#/definitions/FileSystemSpecialPath" + } + }, + "required": [ + "type", + "value" + ], + "title": "SpecialFileSystemPath", + "type": "object" + } + ] + }, + "FileSystemSandboxEntry": { + "properties": { + "access": { + "$ref": "#/definitions/FileSystemAccessMode" + }, + "path": { + "$ref": "#/definitions/FileSystemPath" + } + }, + "required": [ + "access", + "path" + ], + "type": "object" + }, + "FileSystemSpecialPath": { + "oneOf": [ + { + "properties": { + "kind": { + "enum": [ + "root" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "RootFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "minimal" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "MinimalFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "current_working_directory" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "CurrentWorkingDirectoryFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "project_roots" + ], + "type": "string" + }, + "subpath": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "kind" + ], + "title": "KindFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "tmpdir" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "TmpdirFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "slash_tmp" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "SlashTmpFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "unknown" + ], + "type": "string" + }, + "path": { + "type": "string" + }, + "subpath": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "kind", + "path" + ], + "type": "object" + } + ] + }, + "PermissionProfile": { + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfileFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalNetworkPermissions" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "PermissionProfileFileSystemPermissions": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "entries" + ], + "type": "object" + }, "SandboxMode": { "enum": [ "read-only", @@ -139,6 +412,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for the forked thread. Cannot be combined with `sandbox`." + }, "sandbox": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index 35a9e5b2a8..e71155242c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -1,6 +1,21 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, "ApprovalsReviewer": { "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `guardian_subagent` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request.", "enum": [ @@ -133,6 +148,217 @@ } ] }, + "FileSystemAccessMode": { + "enum": [ + "read", + "write", + "none" + ], + "type": "string" + }, + "FileSystemPath": { + "oneOf": [ + { + "properties": { + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "enum": [ + "path" + ], + "title": "PathFileSystemPathType", + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "title": "PathFileSystemPath", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": "string" + }, + "type": { + "enum": [ + "glob_pattern" + ], + "title": "GlobPatternFileSystemPathType", + "type": "string" + } + }, + "required": [ + "pattern", + "type" + ], + "title": "GlobPatternFileSystemPath", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "special" + ], + "title": "SpecialFileSystemPathType", + "type": "string" + }, + "value": { + "$ref": "#/definitions/FileSystemSpecialPath" + } + }, + "required": [ + "type", + "value" + ], + "title": "SpecialFileSystemPath", + "type": "object" + } + ] + }, + "FileSystemSandboxEntry": { + "properties": { + "access": { + "$ref": "#/definitions/FileSystemAccessMode" + }, + "path": { + "$ref": "#/definitions/FileSystemPath" + } + }, + "required": [ + "access", + "path" + ], + "type": "object" + }, + "FileSystemSpecialPath": { + "oneOf": [ + { + "properties": { + "kind": { + "enum": [ + "root" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "RootFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "minimal" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "MinimalFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "current_working_directory" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "CurrentWorkingDirectoryFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "project_roots" + ], + "type": "string" + }, + "subpath": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "kind" + ], + "title": "KindFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "tmpdir" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "TmpdirFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "slash_tmp" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "SlashTmpFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "unknown" + ], + "type": "string" + }, + "path": { + "type": "string" + }, + "subpath": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "kind", + "path" + ], + "type": "object" + } + ] + }, "FunctionCallOutputBody": { "anyOf": [ { @@ -325,6 +551,53 @@ } ] }, + "PermissionProfile": { + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfileFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalNetworkPermissions" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "PermissionProfileFileSystemPermissions": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "entries" + ], + "type": "object" + }, "Personality": { "enum": [ "none", @@ -1052,6 +1325,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for the resumed thread. Cannot be combined with `sandbox`." + }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json index 9b1cba6d1a..27c8b40fc5 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json @@ -1,6 +1,21 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, "ApprovalsReviewer": { "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `guardian_subagent` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request.", "enum": [ @@ -85,6 +100,264 @@ ], "type": "object" }, + "FileSystemAccessMode": { + "enum": [ + "read", + "write", + "none" + ], + "type": "string" + }, + "FileSystemPath": { + "oneOf": [ + { + "properties": { + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "enum": [ + "path" + ], + "title": "PathFileSystemPathType", + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "title": "PathFileSystemPath", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": "string" + }, + "type": { + "enum": [ + "glob_pattern" + ], + "title": "GlobPatternFileSystemPathType", + "type": "string" + } + }, + "required": [ + "pattern", + "type" + ], + "title": "GlobPatternFileSystemPath", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "special" + ], + "title": "SpecialFileSystemPathType", + "type": "string" + }, + "value": { + "$ref": "#/definitions/FileSystemSpecialPath" + } + }, + "required": [ + "type", + "value" + ], + "title": "SpecialFileSystemPath", + "type": "object" + } + ] + }, + "FileSystemSandboxEntry": { + "properties": { + "access": { + "$ref": "#/definitions/FileSystemAccessMode" + }, + "path": { + "$ref": "#/definitions/FileSystemPath" + } + }, + "required": [ + "access", + "path" + ], + "type": "object" + }, + "FileSystemSpecialPath": { + "oneOf": [ + { + "properties": { + "kind": { + "enum": [ + "root" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "RootFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "minimal" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "MinimalFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "current_working_directory" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "CurrentWorkingDirectoryFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "project_roots" + ], + "type": "string" + }, + "subpath": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "kind" + ], + "title": "KindFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "tmpdir" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "TmpdirFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "slash_tmp" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "SlashTmpFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "unknown" + ], + "type": "string" + }, + "path": { + "type": "string" + }, + "subpath": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "kind", + "path" + ], + "type": "object" + } + ] + }, + "PermissionProfile": { + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfileFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalNetworkPermissions" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "PermissionProfileFileSystemPermissions": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "entries" + ], + "type": "object" + }, "Personality": { "enum": [ "none", @@ -181,6 +454,17 @@ "null" ] }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Full permissions override for this thread. Cannot be combined with `sandbox`." + }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json index cad1d8b5bc..e9481f18d9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -5,6 +5,17 @@ "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "type": "string" }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, "ApprovalsReviewer": { "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `guardian_subagent` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request.", "enum": [ @@ -98,6 +109,217 @@ ], "type": "object" }, + "FileSystemAccessMode": { + "enum": [ + "read", + "write", + "none" + ], + "type": "string" + }, + "FileSystemPath": { + "oneOf": [ + { + "properties": { + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "enum": [ + "path" + ], + "title": "PathFileSystemPathType", + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "title": "PathFileSystemPath", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": "string" + }, + "type": { + "enum": [ + "glob_pattern" + ], + "title": "GlobPatternFileSystemPathType", + "type": "string" + } + }, + "required": [ + "pattern", + "type" + ], + "title": "GlobPatternFileSystemPath", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "special" + ], + "title": "SpecialFileSystemPathType", + "type": "string" + }, + "value": { + "$ref": "#/definitions/FileSystemSpecialPath" + } + }, + "required": [ + "type", + "value" + ], + "title": "SpecialFileSystemPath", + "type": "object" + } + ] + }, + "FileSystemSandboxEntry": { + "properties": { + "access": { + "$ref": "#/definitions/FileSystemAccessMode" + }, + "path": { + "$ref": "#/definitions/FileSystemPath" + } + }, + "required": [ + "access", + "path" + ], + "type": "object" + }, + "FileSystemSpecialPath": { + "oneOf": [ + { + "properties": { + "kind": { + "enum": [ + "root" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "RootFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "minimal" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "MinimalFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "current_working_directory" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "CurrentWorkingDirectoryFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "project_roots" + ], + "type": "string" + }, + "subpath": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "kind" + ], + "title": "KindFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "tmpdir" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "TmpdirFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "slash_tmp" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "SlashTmpFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "unknown" + ], + "type": "string" + }, + "path": { + "type": "string" + }, + "subpath": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "kind", + "path" + ], + "type": "object" + } + ] + }, "ModeKind": { "description": "Initial collaboration mode to use when the TUI starts.", "enum": [ @@ -113,6 +335,53 @@ ], "type": "string" }, + "PermissionProfile": { + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfileFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalNetworkPermissions" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "PermissionProfileFileSystemPermissions": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "entries" + ], + "type": "object" + }, "Personality": { "enum": [ "none", @@ -555,6 +824,17 @@ "outputSchema": { "description": "Optional JSON Schema used to constrain the final assistant message for this turn." }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "description": "Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`." + }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts index a7ba311803..bd73b3c3c3 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts @@ -5,6 +5,7 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxMode } from "./SandboxMode"; /** @@ -27,7 +28,11 @@ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /** +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, /** + * Full permissions override for the forked thread. Cannot be combined + * with `sandbox`. + */ +permissionProfile?: PermissionProfile | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /** * If true, persist additional rollout EventMsg variants required to * reconstruct a richer thread history on subsequent resume/fork/read. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts index 770344de8e..d245efe4a2 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts @@ -7,6 +7,7 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxMode } from "./SandboxMode"; /** @@ -36,7 +37,11 @@ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /** +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, /** + * Full permissions override for the resumed thread. Cannot be combined + * with `sandbox`. + */ +permissionProfile?: PermissionProfile | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /** * If true, persist additional rollout EventMsg variants required to * reconstruct a richer thread history on subsequent resume/fork/read. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts index 904487e81c..8b9dafec9f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts @@ -6,6 +6,7 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxMode } from "./SandboxMode"; import type { ThreadStartSource } from "./ThreadStartSource"; @@ -13,7 +14,11 @@ export type ThreadStartParams = {model?: string | null, modelProvider?: string | * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null, /** +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, /** + * Full permissions override for this thread. Cannot be combined with + * `sandbox`. + */ +permissionProfile?: PermissionProfile | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null, /** * If true, opt into emitting raw Responses API items on the event stream. * This is for internal use only (e.g. Codex Cloud). */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts index 8f57a5e68b..3d12e6001c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts @@ -9,6 +9,7 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { UserInput } from "./UserInput"; @@ -26,6 +27,10 @@ approvalsReviewer?: ApprovalsReviewer | null, /** * Override the sandbox policy for this turn and subsequent turns. */ sandboxPolicy?: SandboxPolicy | null, /** + * Override the full permissions profile for this turn and subsequent + * turns. Cannot be combined with `sandboxPolicy`. + */ +permissionProfile?: PermissionProfile | null, /** * Override the model for this turn and subsequent turns. */ model?: string | null, /** diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index c558e002a1..caf7b2bf66 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -3124,6 +3124,10 @@ pub struct ThreadStartParams { pub approvals_reviewer: Option, #[ts(optional = nullable)] pub sandbox: Option, + /// Full permissions override for this thread. Cannot be combined with + /// `sandbox`. + #[ts(optional = nullable)] + pub permission_profile: Option, #[ts(optional = nullable)] pub config: Option>, #[ts(optional = nullable)] @@ -3250,6 +3254,10 @@ pub struct ThreadResumeParams { pub approvals_reviewer: Option, #[ts(optional = nullable)] pub sandbox: Option, + /// Full permissions override for the resumed thread. Cannot be combined + /// with `sandbox`. + #[ts(optional = nullable)] + pub permission_profile: Option, #[ts(optional = nullable)] pub config: Option>, #[ts(optional = nullable)] @@ -3331,6 +3339,10 @@ pub struct ThreadForkParams { pub approvals_reviewer: Option, #[ts(optional = nullable)] pub sandbox: Option, + /// Full permissions override for the forked thread. Cannot be combined + /// with `sandbox`. + #[ts(optional = nullable)] + pub permission_profile: Option, #[ts(optional = nullable)] pub config: Option>, #[ts(optional = nullable)] @@ -4734,6 +4746,10 @@ pub struct TurnStartParams { /// Override the sandbox policy for this turn and subsequent turns. #[ts(optional = nullable)] pub sandbox_policy: Option, + /// Override the full permissions profile for this turn and subsequent + /// turns. Cannot be combined with `sandboxPolicy`. + #[ts(optional = nullable)] + pub permission_profile: Option, /// Override the model for this turn and subsequent turns. #[ts(optional = nullable)] pub model: Option, @@ -9832,6 +9848,20 @@ mod tests { "approvalPolicy": "on-failure", "approvalsReviewer": "user", "sandbox": { "type": "dangerFullAccess" }, + "permissionProfile": { + "network": { "enabled": true }, + "fileSystem": { + "entries": [ + { + "path": { + "type": "special", + "value": { "kind": "root" } + }, + "access": "write" + } + ] + } + }, "reasoningEffort": null }); @@ -9871,6 +9901,7 @@ mod tests { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, model: None, service_tier: None, effort: None, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 7e23cc24a7..4099fcb147 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -82,7 +82,7 @@ Use the thread APIs to create, list, or archive conversations. Drive a conversat - Initialize once per connection: Immediately after opening a transport connection, send an `initialize` request with your client metadata, then emit an `initialized` notification. Any other request on that connection before this handshake gets rejected. - Start (or resume) a thread: Call `thread/start` to open a fresh conversation. The response returns the thread object and you’ll also get a `thread/started` notification. If you’re continuing an existing conversation, call `thread/resume` with its ID instead. If you want to branch from an existing conversation, call `thread/fork` to create a new thread id with copied history. Like `thread/start`, `thread/fork` also accepts `ephemeral: true` for an in-memory temporary thread. The returned `thread.ephemeral` flag tells you whether the session is intentionally in-memory only; when it is `true`, `thread.path` is `null`. -- Begin a turn: To send user input, call `turn/start` with the target `threadId` and the user's input. Optional fields let you override model, cwd, sandbox policy, approval policy, approvals reviewer, etc. This immediately returns the new turn object. The app-server emits `turn/started` when that turn actually begins running. +- Begin a turn: To send user input, call `turn/start` with the target `threadId` and the user's input. Optional fields let you override model, cwd, sandbox policy or `permissionProfile`, approval policy, approvals reviewer, etc. This immediately returns the new turn object. The app-server emits `turn/started` when that turn actually begins running. - Stream events: After `turn/start`, keep reading JSON-RPC notifications on stdout. You’ll see `item/started`, `item/completed`, deltas like `item/agentMessage/delta`, tool progress, etc. These represent streaming model output plus any side effects (commands, tool calls, reasoning notes). - Finish the turn: When the model is done (or the turn is interrupted via making the `turn/interrupt` call), the server sends `turn/completed` with the final turn state and token usage. @@ -136,9 +136,9 @@ Example with notification opt-out: ## API Overview -- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. -- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. -- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. +- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. For permissions, prefer `permissionProfile`; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissionProfile`. +- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`. +- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Accepts the same permission override rules as `thread/start`. - `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. - `thread/loaded/list` — list the thread ids currently loaded in memory. - `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. @@ -155,7 +155,7 @@ Example with notification opt-out: - `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream. - `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted. - `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success. -- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". +- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. Prefer `permissionProfile` for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissionProfile`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". - `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success. - `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. Review and manual compaction turns reject `turn/steer`. - `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`. @@ -223,6 +223,8 @@ Start a fresh thread when you need a new Codex conversation. "cwd": "/Users/me/project", "approvalPolicy": "never", "sandbox": "workspaceWrite", + // Prefer "permissionProfile" for full permission overrides. Do not send + // both "sandbox" and "permissionProfile". "personality": "friendly", "serviceName": "my_app_server_client", // optional metrics tag (`service_name`) "sessionStartSource": "startup", // optional: "startup" (default) or "clear" @@ -539,6 +541,8 @@ You can optionally specify config overrides on the new turn. If specified, these "writableRoots": ["/Users/me/project"], "networkAccess": true }, + // Prefer "permissionProfile" for full permission overrides. Do not send + // both "sandboxPolicy" and "permissionProfile". "model": "gpt-5.1-codex", "effort": "medium", "summary": "concise", diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 3a5fbccc08..ac9b9c1c69 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -102,6 +102,7 @@ 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::PermissionProfile as ApiPermissionProfile; use codex_app_server_protocol::PluginDetail; use codex_app_server_protocol::PluginInstallParams; use codex_app_server_protocol::PluginInstallResponse; @@ -2326,6 +2327,7 @@ impl CodexMessageProcessor { approval_policy, approvals_reviewer, sandbox, + permission_profile, config, service_name, base_instructions, @@ -2338,6 +2340,14 @@ impl CodexMessageProcessor { session_start_source, persist_extended_history, } = params; + if sandbox.is_some() && permission_profile.is_some() { + self.send_invalid_request_error( + request_id, + "`permissionProfile` cannot be combined with `sandbox`".to_string(), + ) + .await; + return; + } let mut typesafe_overrides = self.build_thread_config_overrides( model, model_provider, @@ -2346,6 +2356,7 @@ impl CodexMessageProcessor { approval_policy, approvals_reviewer, sandbox, + permission_profile, base_instructions, developer_instructions, personality, @@ -2747,6 +2758,7 @@ impl CodexMessageProcessor { approval_policy: Option, approvals_reviewer: Option, sandbox: Option, + permission_profile: Option, base_instructions: Option, developer_instructions: Option, personality: Option, @@ -2761,6 +2773,7 @@ impl CodexMessageProcessor { approvals_reviewer: approvals_reviewer .map(codex_app_server_protocol::ApprovalsReviewer::to_core), sandbox_mode: sandbox.map(SandboxMode::to_core), + permission_profile: permission_profile.map(Into::into), codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(), main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(), base_instructions, @@ -4276,12 +4289,21 @@ impl CodexMessageProcessor { approval_policy, approvals_reviewer, sandbox, + permission_profile, config: mut request_overrides, base_instructions, developer_instructions, personality, persist_extended_history, } = params; + if sandbox.is_some() && permission_profile.is_some() { + self.send_invalid_request_error( + request_id, + "`permissionProfile` cannot be combined with `sandbox`".to_string(), + ) + .await; + return; + } let thread_history = if let Some(history) = history { let Some(thread_history) = self @@ -4310,6 +4332,7 @@ impl CodexMessageProcessor { approval_policy, approvals_reviewer, sandbox, + permission_profile, base_instructions, developer_instructions, personality, @@ -4808,12 +4831,21 @@ impl CodexMessageProcessor { approval_policy, approvals_reviewer, sandbox, + permission_profile, config: cli_overrides, base_instructions, developer_instructions, ephemeral, persist_extended_history, } = params; + if sandbox.is_some() && permission_profile.is_some() { + self.send_invalid_request_error( + request_id, + "`permissionProfile` cannot be combined with `sandbox`".to_string(), + ) + .await; + return; + } let (rollout_path, source_thread_id) = if let Some(path) = path { (path, None) @@ -4890,6 +4922,7 @@ impl CodexMessageProcessor { approval_policy, approvals_reviewer, sandbox, + permission_profile, base_instructions, developer_instructions, /*personality*/ None, @@ -7179,6 +7212,7 @@ impl CodexMessageProcessor { || params.approval_policy.is_some() || params.approvals_reviewer.is_some() || params.sandbox_policy.is_some() + || params.permission_profile.is_some() || params.model.is_some() || params.service_tier.is_some() || params.effort.is_some() @@ -7186,6 +7220,15 @@ impl CodexMessageProcessor { || collaboration_mode.is_some() || params.personality.is_some(); + if params.sandbox_policy.is_some() && params.permission_profile.is_some() { + self.send_invalid_request_error( + request_id, + "`permissionProfile` cannot be combined with `sandboxPolicy`".to_string(), + ) + .await; + return; + } + // If any overrides are provided, update the session turn context first. if has_any_overrides { let _ = self @@ -7199,6 +7242,7 @@ impl CodexMessageProcessor { .approvals_reviewer .map(codex_app_server_protocol::ApprovalsReviewer::to_core), sandbox_policy: params.sandbox_policy.map(|p| p.to_core()), + permission_profile: params.permission_profile.map(Into::into), windows_sandbox_level: None, model: params.model, effort: params.effort.map(Some), @@ -9114,6 +9158,16 @@ fn collect_resume_override_mismatches( )); } } + if let Some(requested_permission_profile) = request.permission_profile.as_ref() { + let requested_permission_profile = + codex_protocol::models::PermissionProfile::from(requested_permission_profile.clone()); + if requested_permission_profile != config_snapshot.permission_profile { + mismatch_details.push(format!( + "permission_profile requested={requested_permission_profile:?} active={:?}", + config_snapshot.permission_profile + )); + } + } if let Some(requested_personality) = request.personality.as_ref() && config_snapshot.personality.as_ref() != Some(requested_personality) { @@ -10657,6 +10711,7 @@ mod tests { approval_policy: None, approvals_reviewer: None, sandbox: None, + permission_profile: None, config: None, base_instructions: None, developer_instructions: None, diff --git a/codex-rs/app-server/src/message_processor/tracing_tests.rs b/codex-rs/app-server/src/message_processor/tracing_tests.rs index e42dd5afbc..a1ca9c89ab 100644 --- a/codex-rs/app-server/src/message_processor/tracing_tests.rs +++ b/codex-rs/app-server/src/message_processor/tracing_tests.rs @@ -716,6 +716,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> { cwd: None, approval_policy: None, sandbox_policy: None, + permission_profile: None, approvals_reviewer: None, model: None, service_tier: None, diff --git a/codex-rs/app-server/tests/suite/v2/skills_list.rs b/codex-rs/app-server/tests/suite/v2/skills_list.rs index 8675b3a429..195c4a86db 100644 --- a/codex-rs/app-server/tests/suite/v2/skills_list.rs +++ b/codex-rs/app-server/tests/suite/v2/skills_list.rs @@ -315,6 +315,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<( approval_policy: None, approvals_reviewer: None, sandbox: None, + permission_profile: None, config: None, service_name: None, base_instructions: None, diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 65f8442b0d..b8e3182fd2 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -1749,6 +1749,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }), + permission_profile: None, model: Some("mock-model".to_string()), effort: Some(ReasoningEffort::Medium), summary: Some(ReasoningSummary::Auto), @@ -1783,6 +1784,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { approval_policy: Some(codex_app_server_protocol::AskForApproval::Never), approvals_reviewer: None, sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess), + permission_profile: None, model: Some("mock-model".to_string()), effort: Some(ReasoningEffort::Medium), summary: Some(ReasoningSummary::Auto), diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index e7c7aa31dd..897e1bf611 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -53,6 +53,9 @@ use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; use codex_model_provider_info::WireApi; use codex_models_manager::bundled_models_response; +use codex_protocol::models::FileSystemPermissions; +use codex_protocol::models::NetworkPermissions; +use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -61,6 +64,7 @@ use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::RealtimeVoice; +use codex_protocol::protocol::SandboxPolicy; use serde::Deserialize; use tempfile::tempdir; @@ -782,6 +786,44 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std:: Ok(()) } +#[tokio::test] +async fn permission_profile_override_populates_runtime_permissions() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let permission_profile = PermissionProfile { + network: Some(NetworkPermissions { + enabled: Some(true), + }), + file_system: Some(FileSystemPermissions { + entries: vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Write, + }], + glob_scan_max_depth: None, + }), + }; + + let config = Config::load_from_base_config_with_overrides( + ConfigToml::default(), + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + permission_profile: Some(permission_profile.clone()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + assert_eq!(config.permissions.permission_profile(), permission_profile); + assert_eq!( + config.permissions.sandbox_policy.get(), + &SandboxPolicy::DangerFullAccess + ); + Ok(()) +} + #[tokio::test] async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::io::Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index d81916d66a..97f7808b05 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1374,6 +1374,7 @@ pub struct ConfigOverrides { pub approval_policy: Option, pub approvals_reviewer: Option, pub sandbox_mode: Option, + pub permission_profile: Option, pub model_provider: Option, pub service_tier: Option>, pub config_profile: Option, @@ -1591,6 +1592,7 @@ impl Config { approval_policy: approval_policy_override, approvals_reviewer: approvals_reviewer_override, sandbox_mode, + permission_profile, model_provider, service_tier: service_tier_override, config_profile: config_profile_key, @@ -1611,6 +1613,13 @@ impl Config { additional_writable_roots, } = overrides; + if sandbox_mode.is_some() && permission_profile.is_some() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "`sandbox_mode` and `permission_profile` overrides cannot both be set", + )); + } + let active_profile_name = config_profile_key .as_ref() .or(cfg.profile.as_ref()) @@ -1731,7 +1740,34 @@ impl Config { sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, - ) = if profiles_are_active { + ) = if let Some(permission_profile) = permission_profile { + let configured_network_proxy_config = NetworkProxyConfig::default(); + let (mut file_system_sandbox_policy, network_sandbox_policy) = + permission_profile.to_runtime_permissions(); + let mut sandbox_policy = permission_profile + .to_legacy_sandbox_policy(resolved_cwd.as_path()) + .map_err(|err| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("invalid permission_profile override: {err}"), + ) + })?; + if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) { + file_system_sandbox_policy = file_system_sandbox_policy + .with_additional_writable_roots( + resolved_cwd.as_path(), + &additional_writable_roots, + ); + sandbox_policy = file_system_sandbox_policy + .to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?; + } + ( + configured_network_proxy_config, + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + ) + } else if profiles_are_active { let permissions = cfg.permissions.as_ref().ok_or_else(|| { std::io::Error::new( std::io::ErrorKind::InvalidInput, diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index 921d528cd3..ba877935d0 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -157,6 +157,7 @@ pub(super) async fn user_input_or_turn_inner( approval_policy: Some(approval_policy), approvals_reviewer, sandbox_policy: Some(sandbox_policy), + permission_profile: None, windows_sandbox_level: None, collaboration_mode, reasoning_summary: summary, @@ -1051,6 +1052,7 @@ pub(super) async fn submission_loop( approval_policy, approvals_reviewer, sandbox_policy, + permission_profile, windows_sandbox_level, model, effort, @@ -1077,6 +1079,7 @@ pub(super) async fn submission_loop( approval_policy, approvals_reviewer, sandbox_policy, + permission_profile, windows_sandbox_level, collaboration_mode: Some(collaboration_mode), reasoning_summary: summary, diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 876008d27a..ec5391665a 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1,4 +1,5 @@ use super::*; +use crate::config::ConstraintError; use tokio::sync::Semaphore; /// Context for an initialized model agent @@ -139,13 +140,6 @@ impl SessionConfiguration { if let Some(approvals_reviewer) = updates.approvals_reviewer { next_configuration.approvals_reviewer = approvals_reviewer; } - let mut sandbox_policy_changed = false; - if let Some(sandbox_policy) = updates.sandbox_policy.clone() { - next_configuration.sandbox_policy.set(sandbox_policy)?; - next_configuration.network_sandbox_policy = - NetworkSandboxPolicy::from(next_configuration.sandbox_policy.get()); - sandbox_policy_changed = true; - } if let Some(windows_sandbox_level) = updates.windows_sandbox_level { next_configuration.windows_sandbox_level = windows_sandbox_level; } @@ -166,13 +160,33 @@ impl SessionConfiguration { let cwd_changed = absolute_cwd.as_path() != self.cwd.as_path(); next_configuration.cwd = absolute_cwd; - if sandbox_policy_changed { + + if let Some(permission_profile) = updates.permission_profile.clone() { + let sandbox_policy = permission_profile + .to_legacy_sandbox_policy(&next_configuration.cwd) + .map_err(|err| ConstraintError::InvalidValue { + field_name: "permission_profile", + candidate: format!("{permission_profile:?}"), + allowed: format!( + "permission profiles that can be represented by the active sandbox constraints: {err}" + ), + requirement_source: codex_config::RequirementSource::Unknown, + })?; + next_configuration.sandbox_policy.set(sandbox_policy)?; + let (file_system_sandbox_policy, network_sandbox_policy) = + permission_profile.to_runtime_permissions(); + next_configuration.file_system_sandbox_policy = file_system_sandbox_policy; + next_configuration.network_sandbox_policy = network_sandbox_policy; + } else if let Some(sandbox_policy) = updates.sandbox_policy.clone() { + next_configuration.sandbox_policy.set(sandbox_policy)?; next_configuration.file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries( next_configuration.sandbox_policy.get(), &next_configuration.cwd, &self.file_system_sandbox_policy, ); + next_configuration.network_sandbox_policy = + NetworkSandboxPolicy::from(next_configuration.sandbox_policy.get()); } else if cwd_changed && file_system_policy_matches_legacy { // Preserve richer split policies across cwd-only updates; only // rederive when the session is already using the legacy bridge. @@ -198,6 +212,7 @@ pub(crate) struct SessionSettingsUpdate { pub(crate) approval_policy: Option, pub(crate) approvals_reviewer: Option, pub(crate) sandbox_policy: Option, + pub(crate) permission_profile: Option, pub(crate) windows_sandbox_level: Option, pub(crate) collaboration_mode: Option, pub(crate) reasoning_summary: Option, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 0b936195fd..2034593420 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -1542,6 +1542,7 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result< approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -3694,6 +3695,7 @@ fn op_kind_distinguishes_turn_ops() { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index 57ffb35e60..28685895ac 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -127,6 +127,7 @@ async fn user_input_includes_collaboration_instructions_after_override() -> Resu approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -226,6 +227,7 @@ async fn override_then_next_turn_uses_updated_collaboration_instructions() -> Re approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -279,6 +281,7 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -350,6 +353,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -378,6 +382,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -435,6 +440,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -463,6 +469,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -519,6 +526,7 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -550,6 +558,7 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -610,6 +619,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged() approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -641,6 +651,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged() approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -707,6 +718,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -772,6 +784,7 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 5468151b47..7a4dc525d9 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -3053,6 +3053,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 30e3c54c94..584c5e9507 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -2070,6 +2070,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -2183,6 +2184,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some(next_model.to_string()), effort: None, diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index efa39aaeeb..d12e0c5e89 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -575,6 +575,7 @@ async fn snapshot_rollback_followup_turn_trims_context_updates() -> Result<()> { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/model_overrides.rs b/codex-rs/core/tests/suite/model_overrides.rs index f7fe7cc322..9f8835f19f 100644 --- a/codex-rs/core/tests/suite/model_overrides.rs +++ b/codex-rs/core/tests/suite/model_overrides.rs @@ -30,6 +30,7 @@ async fn override_turn_context_does_not_persist_when_config_exists() { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some("o3".to_string()), effort: Some(Some(ReasoningEffort::High)), @@ -68,6 +69,7 @@ async fn override_turn_context_does_not_create_config_file() { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some("o3".to_string()), effort: Some(Some(ReasoningEffort::Medium)), diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 139ee7a856..5bea4de2a9 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -146,6 +146,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some(next_model.to_string()), effort: None, @@ -243,6 +244,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some(next_model.to_string()), effort: None, @@ -1025,6 +1027,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some(smaller_model_slug.to_string()), effort: None, diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index 0f41637ea2..bee3f3245b 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -451,6 +451,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() - approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some("gpt-5.2".to_string()), effort: None, diff --git a/codex-rs/core/tests/suite/override_updates.rs b/codex-rs/core/tests/suite/override_updates.rs index 13ace1fe6d..63d2b6ed43 100644 --- a/codex-rs/core/tests/suite/override_updates.rs +++ b/codex-rs/core/tests/suite/override_updates.rs @@ -118,6 +118,7 @@ async fn override_turn_context_without_user_turn_does_not_record_permissions_upd approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -160,6 +161,7 @@ async fn override_turn_context_without_user_turn_does_not_record_environment_upd approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -199,6 +201,7 @@ async fn override_turn_context_without_user_turn_does_not_record_collaboration_u approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index d7ceb25fef..5114c8c3c8 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -103,6 +103,7 @@ async fn permissions_message_added_on_override_change() -> Result<()> { approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -231,6 +232,7 @@ async fn permissions_message_omitted_when_disabled() -> Result<()> { approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -317,6 +319,7 @@ async fn resume_replays_permissions_messages() -> Result<()> { approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -419,6 +422,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 251aad76c0..7e729ef493 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -347,6 +347,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -452,6 +453,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -570,6 +572,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -848,6 +851,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 63a21ce1d5..80dec74c3b 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -437,6 +437,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: Some(new_policy.clone()), + permission_profile: None, windows_sandbox_level: None, model: None, effort: Some(Some(ReasoningEffort::High)), @@ -521,6 +522,7 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some("gpt-5.4".to_string()), effort: Some(Some(ReasoningEffort::Low)), diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 9b157bde5f..42b655aaa9 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -590,6 +590,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some(REMOTE_MODEL_SLUG.to_string()), effort: None, @@ -834,6 +835,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some(model.to_string()), effort: None, diff --git a/codex-rs/core/tests/suite/resume.rs b/codex-rs/core/tests/suite/resume.rs index 55d7e45f0a..ce6f2a29f1 100644 --- a/codex-rs/core/tests/suite/resume.rs +++ b/codex-rs/core/tests/suite/resume.rs @@ -422,6 +422,7 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: Some("gpt-5.4".to_string()), effort: None, diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index f4da7c549b..2621fdecef 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -838,6 +838,7 @@ async fn review_uses_overridden_cwd_for_base_branch_merge_base() { approval_policy: None, approvals_reviewer: None, sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 1279532daf..762a4beb6d 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -391,6 +391,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_mode, + permission_profile: None, cwd: resolved_cwd, model_provider: model_provider.clone(), service_tier: None, @@ -749,6 +750,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { approval_policy: Some(default_approval_policy.into()), approvals_reviewer: None, sandbox_policy: Some(default_sandbox_policy.clone().into()), + permission_profile: None, model: None, service_tier: None, effort: default_effort, diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index 46f8ca61ff..11f0be82ab 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -422,6 +422,18 @@ fn session_configured_from_thread_response_uses_review_policy_from_response() { exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }, + permission_profile: codex_app_server_protocol::PermissionProfile::from( + codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( + &codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }, + std::path::Path::new("/tmp"), + ), + ), reasoning_effort: None, }; diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index a97fc70702..551723c773 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -318,6 +318,13 @@ impl PermissionProfile { self.file_system_sandbox_policy() .to_legacy_sandbox_policy(self.network_sandbox_policy(), cwd) } + + pub fn to_runtime_permissions(&self) -> (FileSystemSandboxPolicy, NetworkSandboxPolicy) { + ( + self.file_system_sandbox_policy(), + self.network_sandbox_policy(), + ) + } } impl From for NetworkPermissions { diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index f1c6046c7b..2b1338f812 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -38,6 +38,7 @@ use crate::message_history::HistoryEntry; use crate::models::BaseInstructions; use crate::models::ContentItem; use crate::models::MessagePhase; +use crate::models::PermissionProfile; use crate::models::ResponseInputItem; use crate::models::ResponseItem; use crate::models::WebSearchAction; @@ -519,6 +520,10 @@ pub enum Op { #[serde(skip_serializing_if = "Option::is_none")] sandbox_policy: Option, + /// Updated permissions profile for tool calls. + #[serde(skip_serializing_if = "Option::is_none")] + permission_profile: Option, + /// Updated Windows sandbox mode for tool execution. #[serde(skip_serializing_if = "Option::is_none")] windows_sandbox_level: Option, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index c498d3d41d..a1a658a24d 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -1651,6 +1651,7 @@ async fn update_feature_flags_enabling_guardian_selects_guardian_approvals() -> approval_policy: Some(guardian_approvals.approval_policy), approvals_reviewer: Some(guardian_approvals.approvals_reviewer), sandbox_policy: Some(guardian_approvals.sandbox_policy.clone()), + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -1742,6 +1743,7 @@ async fn update_feature_flags_disabling_guardian_clears_review_policy_and_restor approval_policy: None, approvals_reviewer: Some(ApprovalsReviewer::User), sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -1821,6 +1823,7 @@ async fn update_feature_flags_enabling_guardian_overrides_explicit_manual_review approval_policy: Some(guardian_approvals.approval_policy), approvals_reviewer: Some(guardian_approvals.approvals_reviewer), sandbox_policy: Some(guardian_approvals.sandbox_policy.clone()), + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -1878,6 +1881,7 @@ async fn update_feature_flags_disabling_guardian_clears_manual_review_policy_wit approval_policy: None, approvals_reviewer: Some(ApprovalsReviewer::User), sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -1937,6 +1941,7 @@ async fn update_feature_flags_enabling_guardian_in_profile_sets_profile_auto_rev approval_policy: Some(guardian_approvals.approval_policy), approvals_reviewer: Some(guardian_approvals.approvals_reviewer), sandbox_policy: Some(guardian_approvals.sandbox_policy.clone()), + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -2024,6 +2029,7 @@ guardian_approval = true approval_policy: None, approvals_reviewer: Some(ApprovalsReviewer::User), sandbox_policy: None, + permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/tui/src/app_command.rs b/codex-rs/tui/src/app_command.rs index e94dced053..c34df5b765 100644 --- a/codex-rs/tui/src/app_command.rs +++ b/codex-rs/tui/src/app_command.rs @@ -183,6 +183,7 @@ impl AppCommand { approval_policy, approvals_reviewer, sandbox_policy, + permission_profile: None, windows_sandbox_level, model, effort, @@ -315,6 +316,7 @@ impl AppCommand { approval_policy, approvals_reviewer, sandbox_policy, + permission_profile: _, windows_sandbox_level, model, effort, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 4d8e213ef7..1dd8fc1bf1 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -536,6 +536,7 @@ impl AppServerSession { approval_policy: Some(approval_policy.into()), approvals_reviewer: Some(approvals_reviewer.into()), sandbox_policy: Some(sandbox_policy.into()), + permission_profile: None, model: Some(model), service_tier, effort, @@ -1503,6 +1504,12 @@ mod tests { approval_policy: codex_protocol::protocol::AskForApproval::Never.into(), approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::User, sandbox: codex_protocol::protocol::SandboxPolicy::new_read_only_policy().into(), + permission_profile: + codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( + &codex_protocol::protocol::SandboxPolicy::new_read_only_policy(), + std::path::Path::new("/tmp/project"), + ) + .into(), reasoning_effort: None, }; diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index c1e142c4e1..92ca170605 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -632,6 +632,7 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context approval_policy: Some(AskForApproval::OnRequest), approvals_reviewer: Some(ApprovalsReviewer::GuardianSubagent), sandbox_policy: Some(SandboxPolicy::new_workspace_write_policy()), + permission_profile: None, windows_sandbox_level: None, model: None, effort: None,