mirror of
https://github.com/openai/codex.git
synced 2026-05-14 08:12:36 +00:00
## Why Config loading had become split across crates: `codex-config` owned the config types and merge logic, while `codex-core` still owned the loader that assembled the layer stack. This change consolidates that responsibility in `codex-config`, so the crate that defines config behavior also owns how configs are discovered and loaded. To make that move possible without reintroducing the old dependency cycle, the shell-environment policy types and helpers that `codex-exec-server` needs now live in `codex-protocol` instead of flowing through `codex-config`. This also makes the migrated loader tests more deterministic on machines that already have managed or system Codex config installed by letting tests override the system config and requirements paths instead of reading the host's `/etc/codex`. ## What Changed - moved the config loader implementation from `codex-core` into `codex-config::loader` and deleted the old `core::config_loader` module instead of leaving a compatibility shim - moved shell-environment policy types and helpers into `codex-protocol`, then updated `codex-exec-server` and other downstream crates to import them from their new home - updated downstream callers to use loader/config APIs from `codex-config` - added test-only loader overrides for system config and requirements paths so loader-focused tests do not depend on host-managed config state - cleaned up now-unused dependency entries and platform-specific cfgs that were surfaced by post-push CI ## Testing - `cargo test -p codex-config` - `cargo test -p codex-core config_loader_tests::` - `cargo test -p codex-protocol -p codex-exec-server -p codex-cloud-requirements -p codex-rmcp-client --lib` - `cargo test --lib -p codex-app-server-client -p codex-exec` - `cargo test --no-run --lib -p codex-app-server` - `cargo test -p codex-linux-sandbox --lib` - `cargo shear` - `just bazel-lock-check` ## Notes - I did not chase unrelated full-suite failures outside the migrated loader surface. - `cargo test -p codex-core --lib` still hits unrelated proxy-sensitive failures on this machine, and Windows CI still shows unrelated long-running/timeouting test noise outside the loader migration itself.
444 lines
14 KiB
Rust
444 lines
14 KiB
Rust
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
|
|
use crate::FileSystemSandboxContext;
|
|
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
|
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
|
|
use crate::ProcessId;
|
|
|
|
pub const INITIALIZE_METHOD: &str = "initialize";
|
|
pub const INITIALIZED_METHOD: &str = "initialized";
|
|
pub const EXEC_METHOD: &str = "process/start";
|
|
pub const EXEC_READ_METHOD: &str = "process/read";
|
|
pub const EXEC_WRITE_METHOD: &str = "process/write";
|
|
pub const EXEC_TERMINATE_METHOD: &str = "process/terminate";
|
|
pub const EXEC_OUTPUT_DELTA_METHOD: &str = "process/output";
|
|
pub const EXEC_EXITED_METHOD: &str = "process/exited";
|
|
pub const EXEC_CLOSED_METHOD: &str = "process/closed";
|
|
pub const FS_READ_FILE_METHOD: &str = "fs/readFile";
|
|
pub const FS_WRITE_FILE_METHOD: &str = "fs/writeFile";
|
|
pub const FS_CREATE_DIRECTORY_METHOD: &str = "fs/createDirectory";
|
|
pub const FS_GET_METADATA_METHOD: &str = "fs/getMetadata";
|
|
pub const FS_READ_DIRECTORY_METHOD: &str = "fs/readDirectory";
|
|
pub const FS_REMOVE_METHOD: &str = "fs/remove";
|
|
pub const FS_COPY_METHOD: &str = "fs/copy";
|
|
/// JSON-RPC request method for executor-side HTTP requests.
|
|
pub const HTTP_REQUEST_METHOD: &str = "http/request";
|
|
/// JSON-RPC notification method for streamed executor HTTP response bodies.
|
|
pub const HTTP_REQUEST_BODY_DELTA_METHOD: &str = "http/request/bodyDelta";
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(transparent)]
|
|
pub struct ByteChunk(#[serde(with = "base64_bytes")] pub Vec<u8>);
|
|
|
|
impl ByteChunk {
|
|
pub fn into_inner(self) -> Vec<u8> {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl From<Vec<u8>> for ByteChunk {
|
|
fn from(value: Vec<u8>) -> Self {
|
|
Self(value)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct InitializeParams {
|
|
pub client_name: String,
|
|
#[serde(default)]
|
|
pub resume_session_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct InitializeResponse {
|
|
pub session_id: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecParams {
|
|
/// Client-chosen logical process handle scoped to this connection/session.
|
|
/// This is a protocol key, not an OS pid.
|
|
pub process_id: ProcessId,
|
|
pub argv: Vec<String>,
|
|
pub cwd: PathBuf,
|
|
#[serde(default)]
|
|
pub env_policy: Option<ExecEnvPolicy>,
|
|
pub env: HashMap<String, String>,
|
|
pub tty: bool,
|
|
/// Keep non-tty stdin writable through `process/write`.
|
|
#[serde(default)]
|
|
pub pipe_stdin: bool,
|
|
pub arg0: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecEnvPolicy {
|
|
pub inherit: ShellEnvironmentPolicyInherit,
|
|
pub ignore_default_excludes: bool,
|
|
pub exclude: Vec<String>,
|
|
pub r#set: HashMap<String, String>,
|
|
pub include_only: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecResponse {
|
|
pub process_id: ProcessId,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ReadParams {
|
|
pub process_id: ProcessId,
|
|
pub after_seq: Option<u64>,
|
|
pub max_bytes: Option<usize>,
|
|
pub wait_ms: Option<u64>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ProcessOutputChunk {
|
|
pub seq: u64,
|
|
pub stream: ExecOutputStream,
|
|
pub chunk: ByteChunk,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ReadResponse {
|
|
pub chunks: Vec<ProcessOutputChunk>,
|
|
pub next_seq: u64,
|
|
pub exited: bool,
|
|
pub exit_code: Option<i32>,
|
|
pub closed: bool,
|
|
pub failure: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct WriteParams {
|
|
pub process_id: ProcessId,
|
|
pub chunk: ByteChunk,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum WriteStatus {
|
|
Accepted,
|
|
UnknownProcess,
|
|
StdinClosed,
|
|
Starting,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct WriteResponse {
|
|
pub status: WriteStatus,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct TerminateParams {
|
|
pub process_id: ProcessId,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct TerminateResponse {
|
|
pub running: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsReadFileParams {
|
|
pub path: AbsolutePathBuf,
|
|
pub sandbox: Option<FileSystemSandboxContext>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsReadFileResponse {
|
|
pub data_base64: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsWriteFileParams {
|
|
pub path: AbsolutePathBuf,
|
|
pub data_base64: String,
|
|
pub sandbox: Option<FileSystemSandboxContext>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsWriteFileResponse {}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsCreateDirectoryParams {
|
|
pub path: AbsolutePathBuf,
|
|
pub recursive: Option<bool>,
|
|
pub sandbox: Option<FileSystemSandboxContext>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsCreateDirectoryResponse {}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsGetMetadataParams {
|
|
pub path: AbsolutePathBuf,
|
|
pub sandbox: Option<FileSystemSandboxContext>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsGetMetadataResponse {
|
|
pub is_directory: bool,
|
|
pub is_file: bool,
|
|
pub is_symlink: bool,
|
|
pub created_at_ms: i64,
|
|
pub modified_at_ms: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsReadDirectoryParams {
|
|
pub path: AbsolutePathBuf,
|
|
pub sandbox: Option<FileSystemSandboxContext>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsReadDirectoryEntry {
|
|
pub file_name: String,
|
|
pub is_directory: bool,
|
|
pub is_file: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsReadDirectoryResponse {
|
|
pub entries: Vec<FsReadDirectoryEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsRemoveParams {
|
|
pub path: AbsolutePathBuf,
|
|
pub recursive: Option<bool>,
|
|
pub force: Option<bool>,
|
|
pub sandbox: Option<FileSystemSandboxContext>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsRemoveResponse {}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsCopyParams {
|
|
pub source_path: AbsolutePathBuf,
|
|
pub destination_path: AbsolutePathBuf,
|
|
pub recursive: bool,
|
|
pub sandbox: Option<FileSystemSandboxContext>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FsCopyResponse {}
|
|
|
|
/// HTTP header represented in the executor protocol.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct HttpHeader {
|
|
/// Header name as it appears on the HTTP wire.
|
|
pub name: String,
|
|
/// Header value after UTF-8 conversion.
|
|
pub value: String,
|
|
}
|
|
|
|
/// Executor-side HTTP request envelope.
|
|
///
|
|
/// This intentionally stays transport-shaped rather than MCP-shaped so callers
|
|
/// can use it for Streamable HTTP, OAuth discovery, and future executor-owned
|
|
/// HTTP probes without introducing one protocol method per higher-level use.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct HttpRequestParams {
|
|
/// HTTP method, for example `GET`, `POST`, or `DELETE`.
|
|
pub method: String,
|
|
/// Absolute `http://` or `https://` URL.
|
|
pub url: String,
|
|
/// Ordered request headers. Repeated header names are preserved.
|
|
#[serde(default)]
|
|
pub headers: Vec<HttpHeader>,
|
|
/// Optional request body bytes.
|
|
#[serde(default, rename = "bodyBase64")]
|
|
pub body: Option<ByteChunk>,
|
|
/// Request timeout in milliseconds.
|
|
///
|
|
/// Omitted or `null` disables the timeout. A number applies that exact
|
|
/// millisecond deadline.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub timeout_ms: Option<u64>,
|
|
/// Caller-chosen stream id for `http/request/bodyDelta` notifications.
|
|
///
|
|
/// The id must remain unique on a connection until the terminal body delta
|
|
/// arrives, even if the caller stops reading the stream earlier. Buffered
|
|
/// requests still send an id so callers can keep one consistent request
|
|
/// envelope shape.
|
|
pub request_id: String,
|
|
/// Return after response headers and stream the response body as deltas.
|
|
#[serde(default)]
|
|
pub stream_response: bool,
|
|
}
|
|
|
|
/// HTTP response envelope returned from an executor `http/request` call.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct HttpRequestResponse {
|
|
/// Numeric HTTP response status code.
|
|
pub status: u16,
|
|
/// Ordered response headers. Repeated header names are preserved.
|
|
pub headers: Vec<HttpHeader>,
|
|
/// Buffered response body bytes. Empty when `streamResponse` is true.
|
|
#[serde(rename = "bodyBase64")]
|
|
pub body: ByteChunk,
|
|
}
|
|
|
|
/// Ordered response-body frame for `streamResponse` HTTP requests.
|
|
///
|
|
/// Headers are returned in the `http/request` response so the caller can choose
|
|
/// a parser immediately; body bytes then arrive on this notification stream.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct HttpRequestBodyDeltaNotification {
|
|
/// Request id from the streamed `http/request` call.
|
|
pub request_id: String,
|
|
/// Monotonic one-based body frame sequence number.
|
|
pub seq: u64,
|
|
/// Response-body bytes carried by this frame.
|
|
#[serde(rename = "deltaBase64")]
|
|
pub delta: ByteChunk,
|
|
/// Marks response-body EOF. No later deltas are expected for this request.
|
|
#[serde(default)]
|
|
pub done: bool,
|
|
/// Terminal stream error. Set only on the final notification.
|
|
#[serde(default)]
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum ExecOutputStream {
|
|
Stdout,
|
|
Stderr,
|
|
Pty,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecOutputDeltaNotification {
|
|
pub process_id: ProcessId,
|
|
pub seq: u64,
|
|
pub stream: ExecOutputStream,
|
|
pub chunk: ByteChunk,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecExitedNotification {
|
|
pub process_id: ProcessId,
|
|
pub seq: u64,
|
|
pub exit_code: i32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecClosedNotification {
|
|
pub process_id: ProcessId,
|
|
pub seq: u64,
|
|
}
|
|
|
|
mod base64_bytes {
|
|
use super::BASE64_STANDARD;
|
|
use base64::Engine as _;
|
|
use serde::Deserialize;
|
|
use serde::Deserializer;
|
|
use serde::Serializer;
|
|
|
|
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
serializer.serialize_str(&BASE64_STANDARD.encode(bytes))
|
|
}
|
|
|
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let encoded = String::deserialize(deserializer)?;
|
|
BASE64_STANDARD
|
|
.decode(encoded)
|
|
.map_err(serde::de::Error::custom)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::HttpRequestParams;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
#[test]
|
|
fn http_request_timeout_treats_omitted_and_null_as_no_timeout() {
|
|
let omitted: HttpRequestParams = serde_json::from_value(serde_json::json!({
|
|
"method": "GET",
|
|
"url": "https://example.test",
|
|
"requestId": "req-omitted-timeout",
|
|
}))
|
|
.expect("omitted timeout should deserialize");
|
|
let null_timeout: HttpRequestParams = serde_json::from_value(serde_json::json!({
|
|
"method": "GET",
|
|
"url": "https://example.test",
|
|
"requestId": "req-null-timeout",
|
|
"timeoutMs": null,
|
|
}))
|
|
.expect("null timeout should deserialize");
|
|
let explicit_timeout: HttpRequestParams = serde_json::from_value(serde_json::json!({
|
|
"method": "GET",
|
|
"url": "https://example.test",
|
|
"requestId": "req-explicit-timeout",
|
|
"timeoutMs": 1234,
|
|
}))
|
|
.expect("numeric timeout should deserialize");
|
|
|
|
assert_eq!(
|
|
(omitted.request_id.as_str(), omitted.timeout_ms),
|
|
("req-omitted-timeout", None)
|
|
);
|
|
assert_eq!(
|
|
(null_timeout.request_id.as_str(), null_timeout.timeout_ms),
|
|
("req-null-timeout", None)
|
|
);
|
|
assert_eq!(
|
|
(
|
|
explicit_timeout.request_id.as_str(),
|
|
explicit_timeout.timeout_ms
|
|
),
|
|
("req-explicit-timeout", Some(1234))
|
|
);
|
|
}
|
|
}
|