Codex Handbook
app-server/tests/suite/v2/request_permissions.rs 181 lines
use anyhow::Result;use app_test_support::TestAppServer;use app_test_support::create_final_assistant_message_sse_response;use app_test_support::create_mock_responses_server_sequence;use app_test_support::create_request_permissions_sse_response;use app_test_support::to_response;use codex_app_server_protocol::JSONRPCMessage;use codex_app_server_protocol::JSONRPCResponse;use codex_app_server_protocol::PermissionGrantScope;use codex_app_server_protocol::PermissionsRequestApprovalResponse;use codex_app_server_protocol::RequestId;use codex_app_server_protocol::ServerRequest;use codex_app_server_protocol::ServerRequestResolvedNotification;use codex_app_server_protocol::ThreadStartParams;use codex_app_server_protocol::ThreadStartResponse;use codex_app_server_protocol::TurnStartParams;use codex_app_server_protocol::TurnStartResponse;use codex_app_server_protocol::UserInput as V2UserInput;use tokio::time::timeout;const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);#[tokio::test(flavor = "multi_thread", worker_threads = 4)]async fn request_permissions_round_trip() -> Result<()> {    let codex_home = tempfile::TempDir::new()?;    let responses = vec![        create_request_permissions_sse_response("call1")?,        create_final_assistant_message_sse_response("done")?,    ];    let server = create_mock_responses_server_sequence(responses).await;    create_config_toml(codex_home.path(), &server.uri())?;    let mut mcp = TestAppServer::new(codex_home.path()).await?;    timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;    let thread_start_id = mcp        .send_thread_start_request(ThreadStartParams {            model: Some("mock-model".to_string()),            ..Default::default()        })        .await?;    let thread_start_resp: JSONRPCResponse = timeout(        DEFAULT_READ_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(thread_start_id)),    )    .await??;    let ThreadStartResponse { thread, .. } = to_response(thread_start_resp)?;    let turn_start_id = mcp        .send_turn_start_request(TurnStartParams {            thread_id: thread.id.clone(),            client_user_message_id: None,            input: vec![V2UserInput::Text {                text: "pick a directory".to_string(),                text_elements: Vec::new(),            }],            model: Some("mock-model".to_string()),            ..Default::default()        })        .await?;    let turn_start_resp: JSONRPCResponse = timeout(        DEFAULT_READ_TIMEOUT,        mcp.read_stream_until_response_message(RequestId::Integer(turn_start_id)),    )    .await??;    let TurnStartResponse { turn, .. } = to_response(turn_start_resp)?;    let server_req = timeout(        DEFAULT_READ_TIMEOUT,        mcp.read_stream_until_request_message(),    )    .await??;    let ServerRequest::PermissionsRequestApproval { request_id, params } = server_req else {        panic!("expected PermissionsRequestApproval request, got: {server_req:?}");    };    assert_eq!(params.thread_id, thread.id);    assert_eq!(params.turn_id, turn.id);    assert_eq!(params.item_id, "call1");    assert!(params.cwd.as_path().is_absolute());    assert_eq!(params.reason, Some("Select a workspace root".to_string()));    let requested_file_system = params        .permissions        .file_system        .expect("request should include file system permissions");    let requested_writes = requested_file_system        .write        .clone()        .expect("request should include write permissions");    assert_eq!(requested_writes.len(), 2);    assert_eq!(        requested_file_system.entries,        Some(vec![            codex_app_server_protocol::FileSystemSandboxEntry {                path: codex_app_server_protocol::FileSystemPath::Path {                    path: requested_writes[0].clone(),                },                access: codex_app_server_protocol::FileSystemAccessMode::Write,            },            codex_app_server_protocol::FileSystemSandboxEntry {                path: codex_app_server_protocol::FileSystemPath::Path {                    path: requested_writes[1].clone(),                },                access: codex_app_server_protocol::FileSystemAccessMode::Write,            },        ])    );    let resolved_request_id = request_id.clone();    mcp.send_response(        request_id,        serde_json::to_value(PermissionsRequestApprovalResponse {            permissions: codex_app_server_protocol::GrantedPermissionProfile {                network: None,                file_system: Some(codex_app_server_protocol::AdditionalFileSystemPermissions {                    read: None,                    write: Some(vec![requested_writes[0].clone()]),                    glob_scan_max_depth: None,                    entries: None,                }),            },            scope: PermissionGrantScope::Turn,            strict_auto_review: None,        })?,    )    .await?;    let mut saw_resolved = false;    loop {        let message = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??;        let JSONRPCMessage::Notification(notification) = message else {            continue;        };        match notification.method.as_str() {            "serverRequest/resolved" => {                let resolved: ServerRequestResolvedNotification = serde_json::from_value(                    notification                        .params                        .clone()                        .expect("serverRequest/resolved params"),                )?;                assert_eq!(resolved.thread_id, thread.id);                assert_eq!(resolved.request_id, resolved_request_id);                saw_resolved = true;            }            "turn/completed" => {                assert!(saw_resolved, "serverRequest/resolved should arrive first");                break;            }            _ => {}        }    }    Ok(())}fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()> {    let config_toml = codex_home.join("config.toml");    std::fs::write(        config_toml,        format!(            r#"model = "mock-model"approval_policy = "untrusted"sandbox_mode = "read-only"model_provider = "mock_provider"[model_providers.mock_provider]name = "Mock provider for test"base_url = "{server_uri}/v1"wire_api = "responses"request_max_retries = 0stream_max_retries = 0[features]request_permissions_tool = true"#        ),    )}