mirror of
https://github.com/openai/codex.git
synced 2026-05-17 17:53:06 +00:00
app-server: expose thread permission profiles (#18278)
## Why The `PermissionProfile` migration needs app-server clients to see the same constrained permission model that core is using at runtime. Before this PR, thread lifecycle responses only exposed the legacy `SandboxPolicy` shape, so clients still had to infer active permissions from sandbox fields. That makes downstream resume, fork, and override flows harder to make `PermissionProfile`-first. External sandbox policies are intentionally excluded from this canonical view. External enforcement cannot be round-tripped as a `PermissionProfile`, and exposing a lossy root-write profile would let clients accidentally change sandbox semantics if they echo the profile back later. ## What changed - Adds the app-server v2 `PermissionProfile` wire shape, including filesystem permissions and glob scan depth metadata. - Adds `PermissionProfileNetworkPermissions` so the profile response does not expose active network state through the older additional-permissions naming. - Returns `permissionProfile` from thread start, resume, and fork responses when the active sandbox can be represented as a `PermissionProfile`. - Keeps legacy `sandbox` in those responses for compatibility and documents `permissionProfile` as canonical when present. - Makes lifecycle `permissionProfile` nullable and returns `null` for `ExternalSandbox` to avoid exposing a lossy profile. - Regenerates the app-server JSON schema and TypeScript fixtures. ## Verification - `cargo test -p codex-app-server-protocol` - `cargo test -p codex-app-server thread_response_permission_profile_omits_external_sandbox -- --nocapture` - `cargo check --tests -p codex-analytics -p codex-exec -p codex-tui` - `just fix -p codex-app-server-protocol -p codex-app-server -p codex-analytics -p codex-exec -p codex-tui` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18278). * #18279 * __->__ #18278
This commit is contained in:
@@ -1435,6 +1435,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn serialize_client_response() -> Result<()> {
|
||||
let cwd = absolute_path("/tmp");
|
||||
let response = ClientResponse::ThreadStart {
|
||||
request_id: RequestId::Integer(7),
|
||||
response: v2::ThreadStartResponse {
|
||||
@@ -1448,7 +1449,7 @@ mod tests {
|
||||
updated_at: 2,
|
||||
status: v2::ThreadStatus::Idle,
|
||||
path: None,
|
||||
cwd: absolute_path("/tmp"),
|
||||
cwd: cwd.clone(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: v2::SessionSource::Exec,
|
||||
agent_nickname: None,
|
||||
@@ -1460,11 +1461,18 @@ mod tests {
|
||||
model: "gpt-5".to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
cwd: absolute_path("/tmp"),
|
||||
cwd: cwd.clone(),
|
||||
instruction_sources: vec![absolute_path("/tmp/AGENTS.md")],
|
||||
approval_policy: v2::AskForApproval::OnFailure,
|
||||
approvals_reviewer: v2::ApprovalsReviewer::User,
|
||||
sandbox: v2::SandboxPolicy::DangerFullAccess,
|
||||
permission_profile: Some(
|
||||
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
|
||||
&codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
|
||||
cwd.as_path(),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
reasoning_effort: None,
|
||||
},
|
||||
};
|
||||
@@ -1507,6 +1515,24 @@ mod tests {
|
||||
"sandbox": {
|
||||
"type": "dangerFullAccess"
|
||||
},
|
||||
"permissionProfile": {
|
||||
"network": {
|
||||
"enabled": true,
|
||||
},
|
||||
"fileSystem": {
|
||||
"entries": [
|
||||
{
|
||||
"path": {
|
||||
"type": "special",
|
||||
"value": {
|
||||
"kind": "root",
|
||||
},
|
||||
},
|
||||
"access": "write",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"reasoningEffort": null
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1225,6 +1225,13 @@ pub struct AdditionalNetworkPermissions {
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PermissionProfileNetworkPermissions {
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<CoreNetworkPermissions> for AdditionalNetworkPermissions {
|
||||
fn from(value: CoreNetworkPermissions) -> Self {
|
||||
Self {
|
||||
@@ -1241,6 +1248,22 @@ impl From<AdditionalNetworkPermissions> for CoreNetworkPermissions {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CoreNetworkPermissions> for PermissionProfileNetworkPermissions {
|
||||
fn from(value: CoreNetworkPermissions) -> Self {
|
||||
Self {
|
||||
enabled: value.enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PermissionProfileNetworkPermissions> for CoreNetworkPermissions {
|
||||
fn from(value: PermissionProfileNetworkPermissions) -> Self {
|
||||
Self {
|
||||
enabled: value.enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -1383,6 +1406,70 @@ impl From<FileSystemSandboxEntry> for CoreFileSystemSandboxEntry {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PermissionProfileFileSystemPermissions {
|
||||
pub entries: Vec<FileSystemSandboxEntry>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub glob_scan_max_depth: Option<NonZeroUsize>,
|
||||
}
|
||||
|
||||
impl From<CoreFileSystemPermissions> for PermissionProfileFileSystemPermissions {
|
||||
fn from(value: CoreFileSystemPermissions) -> Self {
|
||||
Self {
|
||||
entries: value
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(FileSystemSandboxEntry::from)
|
||||
.collect(),
|
||||
glob_scan_max_depth: value.glob_scan_max_depth,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PermissionProfileFileSystemPermissions> for CoreFileSystemPermissions {
|
||||
fn from(value: PermissionProfileFileSystemPermissions) -> Self {
|
||||
Self {
|
||||
entries: value
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(CoreFileSystemSandboxEntry::from)
|
||||
.collect(),
|
||||
glob_scan_max_depth: value.glob_scan_max_depth,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PermissionProfile {
|
||||
pub network: Option<PermissionProfileNetworkPermissions>,
|
||||
pub file_system: Option<PermissionProfileFileSystemPermissions>,
|
||||
}
|
||||
|
||||
impl From<CorePermissionProfile> for PermissionProfile {
|
||||
fn from(value: CorePermissionProfile) -> Self {
|
||||
Self {
|
||||
network: value.network.map(PermissionProfileNetworkPermissions::from),
|
||||
file_system: value
|
||||
.file_system
|
||||
.map(PermissionProfileFileSystemPermissions::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PermissionProfile> for CorePermissionProfile {
|
||||
fn from(value: PermissionProfile) -> Self {
|
||||
Self {
|
||||
network: value.network.map(CoreNetworkPermissions::from),
|
||||
file_system: value.file_system.map(CoreFileSystemPermissions::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -3127,7 +3214,15 @@ pub struct ThreadStartResponse {
|
||||
pub approval_policy: AskForApproval,
|
||||
/// Reviewer currently used for approval requests on this thread.
|
||||
pub approvals_reviewer: ApprovalsReviewer,
|
||||
/// Legacy sandbox policy retained for compatibility. New clients should use
|
||||
/// `permissionProfile` when present as the canonical active permissions
|
||||
/// view.
|
||||
pub sandbox: SandboxPolicy,
|
||||
/// Canonical active permissions view for this thread when representable.
|
||||
/// This is `null` for external sandbox policies because external
|
||||
/// enforcement cannot be round-tripped as a `PermissionProfile`.
|
||||
#[serde(default)]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
}
|
||||
|
||||
@@ -3216,7 +3311,15 @@ pub struct ThreadResumeResponse {
|
||||
pub approval_policy: AskForApproval,
|
||||
/// Reviewer currently used for approval requests on this thread.
|
||||
pub approvals_reviewer: ApprovalsReviewer,
|
||||
/// Legacy sandbox policy retained for compatibility. New clients should use
|
||||
/// `permissionProfile` when present as the canonical active permissions
|
||||
/// view.
|
||||
pub sandbox: SandboxPolicy,
|
||||
/// Canonical active permissions view for this thread when representable.
|
||||
/// This is `null` for external sandbox policies because external
|
||||
/// enforcement cannot be round-tripped as a `PermissionProfile`.
|
||||
#[serde(default)]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
}
|
||||
|
||||
@@ -3296,7 +3399,15 @@ pub struct ThreadForkResponse {
|
||||
pub approval_policy: AskForApproval,
|
||||
/// Reviewer currently used for approval requests on this thread.
|
||||
pub approvals_reviewer: ApprovalsReviewer,
|
||||
/// Legacy sandbox policy retained for compatibility. New clients should use
|
||||
/// `permissionProfile` when present as the canonical active permissions
|
||||
/// view.
|
||||
pub sandbox: SandboxPolicy,
|
||||
/// Canonical active permissions view for this thread when representable.
|
||||
/// This is `null` for external sandbox policies because external
|
||||
/// enforcement cannot be round-tripped as a `PermissionProfile`.
|
||||
#[serde(default)]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
}
|
||||
|
||||
@@ -7419,6 +7530,47 @@ mod tests {
|
||||
.expect_err("zero glob scan depth should fail deserialization");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_profile_file_system_permissions_preserves_glob_scan_depth() {
|
||||
let core_permissions = CoreFileSystemPermissions {
|
||||
entries: vec![CoreFileSystemSandboxEntry {
|
||||
path: CoreFileSystemPath::GlobPattern {
|
||||
pattern: "**/*.env".to_string(),
|
||||
},
|
||||
access: CoreFileSystemAccessMode::None,
|
||||
}],
|
||||
glob_scan_max_depth: NonZeroUsize::new(2),
|
||||
};
|
||||
|
||||
let permissions = PermissionProfileFileSystemPermissions::from(core_permissions.clone());
|
||||
|
||||
assert_eq!(
|
||||
permissions,
|
||||
PermissionProfileFileSystemPermissions {
|
||||
entries: vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: "**/*.env".to_string(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
}],
|
||||
glob_scan_max_depth: NonZeroUsize::new(2),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
CoreFileSystemPermissions::from(permissions),
|
||||
core_permissions
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_profile_file_system_permissions_rejects_zero_glob_scan_depth() {
|
||||
serde_json::from_value::<PermissionProfileFileSystemPermissions>(json!({
|
||||
"entries": [],
|
||||
"globScanMaxDepth": 0,
|
||||
}))
|
||||
.expect_err("zero glob scan depth should fail deserialization");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_request_approval_response_uses_granted_permission_profile_without_macos() {
|
||||
let read_only_path = if cfg!(windows) {
|
||||
@@ -9708,7 +9860,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_lifecycle_responses_default_missing_instruction_sources() {
|
||||
fn thread_lifecycle_responses_default_missing_compat_fields() {
|
||||
let response = json!({
|
||||
"thread": {
|
||||
"id": "thread-id",
|
||||
@@ -9749,6 +9901,9 @@ mod tests {
|
||||
assert_eq!(start.instruction_sources, Vec::<AbsolutePathBuf>::new());
|
||||
assert_eq!(resume.instruction_sources, Vec::<AbsolutePathBuf>::new());
|
||||
assert_eq!(fork.instruction_sources, Vec::<AbsolutePathBuf>::new());
|
||||
assert_eq!(start.permission_profile, None);
|
||||
assert_eq!(resume.permission_profile, None);
|
||||
assert_eq!(fork.permission_profile, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user