mirror of
https://github.com/openai/codex.git
synced 2026-11-10 08:43:33 +00:00
fix: introduce AbsolutePathBuf and resolve relative paths in config.toml (#7796)
This PR attempts to solve two problems by introducing a `AbsolutePathBuf` type with a special deserializer: - `AbsolutePathBuf` attempts to be a generally useful abstraction, as it ensures, by constructing, that it represents a value that is an absolute, normalized path, which is a stronger guarantee than an arbitrary `PathBuf`. - Values in `config.toml` that can be either an absolute or relative path should be resolved against the folder containing the `config.toml` in the relative path case. This PR makes this easy to support: the main cost is ensuring `AbsolutePathBufGuard` is used inside `deserialize_config_toml_with_base()`. While `AbsolutePathBufGuard` may seem slightly distasteful because it relies on thread-local storage, this seems much cleaner to me than using than my various experiments with https://docs.rs/serde/latest/serde/de/trait.DeserializeSeed.html. Further, since the `deserialize()` method from the `Deserialize` trait is not async, we do not really have to worry about the deserialization work being spread across multiple threads in a way that would interfere with `AbsolutePathBufGuard`. To start, this PR introduces the use of `AbsolutePathBuf` in `OtelTlsConfig`. Note how this simplifies `otel_provider.rs` because it no longer requires `settings.codex_home` to be threaded through. Furthermore, this sets us up better for a world where multiple `config.toml` files from different folders could be loaded and then merged together, as the absolutifying of the paths must be done against the correct parent folder.
This commit is contained in:
1 parent
0c8828c5e2
commit
fa4cac1e6b
10 files changed
+246
-62
No files matched your search
Generated
+12
@@ -1157,6 +1157,7 @@ dependencies = [
|
|||||||
"codex-otel",
|
"codex-otel",
|
||||||
"codex-protocol",
|
"codex-protocol",
|
||||||
"codex-rmcp-client",
|
"codex-rmcp-client",
|
||||||
|
"codex-utils-absolute-path",
|
||||||
"codex-utils-pty",
|
"codex-utils-pty",
|
||||||
"codex-utils-readiness",
|
"codex-utils-readiness",
|
||||||
"codex-utils-string",
|
"codex-utils-string",
|
||||||
@@ -1464,6 +1465,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"codex-app-server-protocol",
|
"codex-app-server-protocol",
|
||||||
"codex-protocol",
|
"codex-protocol",
|
||||||
|
"codex-utils-absolute-path",
|
||||||
"eventsource-stream",
|
"eventsource-stream",
|
||||||
"http",
|
"http",
|
||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
@@ -1650,6 +1652,16 @@ dependencies = [
|
|||||||
"codex-tui",
|
"codex-tui",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "codex-utils-absolute-path"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"path-absolutize",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codex-utils-cache"
|
name = "codex-utils-cache"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ members = [
|
|||||||
"otel",
|
"otel",
|
||||||
"tui",
|
"tui",
|
||||||
"tui2",
|
"tui2",
|
||||||
|
"utils/absolute-path",
|
||||||
"utils/git",
|
"utils/git",
|
||||||
"utils/cache",
|
"utils/cache",
|
||||||
"utils/image",
|
"utils/image",
|
||||||
@@ -90,6 +91,7 @@ codex-rmcp-client = { path = "rmcp-client" }
|
|||||||
codex-stdio-to-uds = { path = "stdio-to-uds" }
|
codex-stdio-to-uds = { path = "stdio-to-uds" }
|
||||||
codex-tui = { path = "tui" }
|
codex-tui = { path = "tui" }
|
||||||
codex-tui2 = { path = "tui2" }
|
codex-tui2 = { path = "tui2" }
|
||||||
|
codex-utils-absolute-path = { path = "utils/absolute-path" }
|
||||||
codex-utils-cache = { path = "utils/cache" }
|
codex-utils-cache = { path = "utils/cache" }
|
||||||
codex-utils-image = { path = "utils/image" }
|
codex-utils-image = { path = "utils/image" }
|
||||||
codex-utils-json-to-toml = { path = "utils/json-to-toml" }
|
codex-utils-json-to-toml = { path = "utils/json-to-toml" }
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "codex-core"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
name = "codex-core"
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
doctest = false
|
doctest = false
|
||||||
@@ -18,12 +18,12 @@ askama = { workspace = true }
|
|||||||
async-channel = { workspace = true }
|
async-channel = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
chrono = { workspace = true, features = ["serde"] }
|
|
||||||
chardetng = { workspace = true }
|
chardetng = { workspace = true }
|
||||||
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
|
codex-api = { workspace = true }
|
||||||
codex-app-server-protocol = { workspace = true }
|
codex-app-server-protocol = { workspace = true }
|
||||||
codex-apply-patch = { workspace = true }
|
codex-apply-patch = { workspace = true }
|
||||||
codex-async-utils = { workspace = true }
|
codex-async-utils = { workspace = true }
|
||||||
codex-api = { workspace = true }
|
|
||||||
codex-execpolicy = { workspace = true }
|
codex-execpolicy = { workspace = true }
|
||||||
codex-file-search = { workspace = true }
|
codex-file-search = { workspace = true }
|
||||||
codex-git = { workspace = true }
|
codex-git = { workspace = true }
|
||||||
@@ -31,14 +31,15 @@ codex-keyring-store = { workspace = true }
|
|||||||
codex-otel = { workspace = true, features = ["otel"] }
|
codex-otel = { workspace = true, features = ["otel"] }
|
||||||
codex-protocol = { workspace = true }
|
codex-protocol = { workspace = true }
|
||||||
codex-rmcp-client = { workspace = true }
|
codex-rmcp-client = { workspace = true }
|
||||||
|
codex-utils-absolute-path = { workspace = true }
|
||||||
codex-utils-pty = { workspace = true }
|
codex-utils-pty = { workspace = true }
|
||||||
codex-utils-readiness = { workspace = true }
|
codex-utils-readiness = { workspace = true }
|
||||||
codex-utils-string = { workspace = true }
|
codex-utils-string = { workspace = true }
|
||||||
codex-windows-sandbox = { package = "codex-windows-sandbox", path = "../windows-sandbox-rs" }
|
codex-windows-sandbox = { package = "codex-windows-sandbox", path = "../windows-sandbox-rs" }
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
dunce = { workspace = true }
|
dunce = { workspace = true }
|
||||||
env-flags = { workspace = true }
|
|
||||||
encoding_rs = { workspace = true }
|
encoding_rs = { workspace = true }
|
||||||
|
env-flags = { workspace = true }
|
||||||
eventsource-stream = { workspace = true }
|
eventsource-stream = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
http = { workspace = true }
|
http = { workspace = true }
|
||||||
@@ -46,8 +47,10 @@ indexmap = { workspace = true }
|
|||||||
keyring = { workspace = true, features = ["crypto-rust"] }
|
keyring = { workspace = true, features = ["crypto-rust"] }
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
mcp-types = { workspace = true }
|
mcp-types = { workspace = true }
|
||||||
|
once_cell = { workspace = true }
|
||||||
os_info = { workspace = true }
|
os_info = { workspace = true }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
|
regex = { workspace = true }
|
||||||
regex-lite = { workspace = true }
|
regex-lite = { workspace = true }
|
||||||
reqwest = { workspace = true, features = ["json", "stream"] }
|
reqwest = { workspace = true, features = ["json", "stream"] }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
@@ -58,9 +61,6 @@ sha2 = { workspace = true }
|
|||||||
shlex = { workspace = true }
|
shlex = { workspace = true }
|
||||||
similar = { workspace = true }
|
similar = { workspace = true }
|
||||||
strum_macros = { workspace = true }
|
strum_macros = { workspace = true }
|
||||||
url = { workspace = true }
|
|
||||||
once_cell = { workspace = true }
|
|
||||||
regex = { workspace = true }
|
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
test-case = "3.3.1"
|
test-case = "3.3.1"
|
||||||
test-log = { workspace = true }
|
test-log = { workspace = true }
|
||||||
@@ -84,6 +84,7 @@ toml_edit = { workspace = true }
|
|||||||
tracing = { workspace = true, features = ["log"] }
|
tracing = { workspace = true, features = ["log"] }
|
||||||
tree-sitter = { workspace = true }
|
tree-sitter = { workspace = true }
|
||||||
tree-sitter-bash = { workspace = true }
|
tree-sitter-bash = { workspace = true }
|
||||||
|
url = { workspace = true }
|
||||||
uuid = { workspace = true, features = ["serde", "v4", "v5"] }
|
uuid = { workspace = true, features = ["serde", "v4", "v5"] }
|
||||||
which = { workspace = true }
|
which = { workspace = true }
|
||||||
wildmatch = { workspace = true }
|
wildmatch = { workspace = true }
|
||||||
@@ -94,9 +95,9 @@ test-support = []
|
|||||||
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
keyring = { workspace = true, features = ["linux-native-async-persistent"] }
|
||||||
landlock = { workspace = true }
|
landlock = { workspace = true }
|
||||||
seccompiler = { workspace = true }
|
seccompiler = { workspace = true }
|
||||||
keyring = { workspace = true, features = ["linux-native-async-persistent"] }
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
core-foundation = "0.9"
|
core-foundation = "0.9"
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ use codex_protocol::config_types::TrustLevel;
|
|||||||
use codex_protocol::config_types::Verbosity;
|
use codex_protocol::config_types::Verbosity;
|
||||||
use codex_protocol::openai_models::ReasoningEffort;
|
use codex_protocol::openai_models::ReasoningEffort;
|
||||||
use codex_rmcp_client::OAuthCredentialsStoreMode;
|
use codex_rmcp_client::OAuthCredentialsStoreMode;
|
||||||
|
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||||
use dirs::home_dir;
|
use dirs::home_dir;
|
||||||
use dunce::canonicalize;
|
use dunce::canonicalize;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -299,9 +300,9 @@ impl Config {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let cfg: ConfigToml = root_value.try_into().map_err(|e| {
|
let cfg = deserialize_config_toml_with_base(root_value, &codex_home).map_err(|e| {
|
||||||
tracing::error!("Failed to deserialize overridden config: {e}");
|
tracing::error!("Failed to deserialize overridden config: {e}");
|
||||||
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
|
e
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Self::load_from_base_config_with_overrides(cfg, overrides, codex_home)
|
Self::load_from_base_config_with_overrides(cfg, overrides, codex_home)
|
||||||
@@ -319,9 +320,9 @@ pub async fn load_config_as_toml_with_cli_overrides(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let cfg: ConfigToml = root_value.try_into().map_err(|e| {
|
let cfg = deserialize_config_toml_with_base(root_value, codex_home).map_err(|e| {
|
||||||
tracing::error!("Failed to deserialize overridden config: {e}");
|
tracing::error!("Failed to deserialize overridden config: {e}");
|
||||||
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
|
e
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(cfg)
|
Ok(cfg)
|
||||||
@@ -357,6 +358,18 @@ fn apply_overlays(
|
|||||||
base
|
base
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deserialize_config_toml_with_base(
|
||||||
|
root_value: TomlValue,
|
||||||
|
config_base_dir: &Path,
|
||||||
|
) -> std::io::Result<ConfigToml> {
|
||||||
|
// This guard ensures that any relative paths that is deserialized into an
|
||||||
|
// [AbsolutePathBuf] is resolved against `config_base_dir`.
|
||||||
|
let _guard = AbsolutePathBufGuard::new(config_base_dir);
|
||||||
|
root_value
|
||||||
|
.try_into()
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn load_global_mcp_servers(
|
pub async fn load_global_mcp_servers(
|
||||||
codex_home: &Path,
|
codex_home: &Path,
|
||||||
) -> std::io::Result<BTreeMap<String, McpServerConfig>> {
|
) -> std::io::Result<BTreeMap<String, McpServerConfig>> {
|
||||||
@@ -1852,10 +1865,11 @@ trust_level = "trusted"
|
|||||||
};
|
};
|
||||||
|
|
||||||
let root_value = load_resolved_config(codex_home.path(), Vec::new(), overrides).await?;
|
let root_value = load_resolved_config(codex_home.path(), Vec::new(), overrides).await?;
|
||||||
let cfg: ConfigToml = root_value.try_into().map_err(|e| {
|
let cfg =
|
||||||
tracing::error!("Failed to deserialize overridden config: {e}");
|
deserialize_config_toml_with_base(root_value, codex_home.path()).map_err(|e| {
|
||||||
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
|
tracing::error!("Failed to deserialize overridden config: {e}");
|
||||||
})?;
|
e
|
||||||
|
})?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cfg.mcp_oauth_credentials_store,
|
cfg.mcp_oauth_credentials_store,
|
||||||
Some(OAuthCredentialsStoreMode::Keyring),
|
Some(OAuthCredentialsStoreMode::Keyring),
|
||||||
@@ -1972,10 +1986,11 @@ trust_level = "trusted"
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let cfg: ConfigToml = root_value.try_into().map_err(|e| {
|
let cfg =
|
||||||
tracing::error!("Failed to deserialize overridden config: {e}");
|
deserialize_config_toml_with_base(root_value, codex_home.path()).map_err(|e| {
|
||||||
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
|
tracing::error!("Failed to deserialize overridden config: {e}");
|
||||||
})?;
|
e
|
||||||
|
})?;
|
||||||
|
|
||||||
assert_eq!(cfg.model.as_deref(), Some("managed_config"));
|
assert_eq!(cfg.model.as_deref(), Some("managed_config"));
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
// Note this file should generally be restricted to simple struct/enum
|
// Note this file should generally be restricted to simple struct/enum
|
||||||
// definitions that do not contain business logic.
|
// definitions that do not contain business logic.
|
||||||
|
|
||||||
use serde::Deserializer;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use wildmatch::WildMatchPattern;
|
use wildmatch::WildMatchPattern;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use serde::Deserializer;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde::de::Error as SerdeError;
|
use serde::de::Error as SerdeError;
|
||||||
|
|
||||||
@@ -285,9 +286,9 @@ pub enum OtelHttpProtocol {
|
|||||||
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
|
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct OtelTlsConfig {
|
pub struct OtelTlsConfig {
|
||||||
pub ca_certificate: Option<PathBuf>,
|
pub ca_certificate: Option<AbsolutePathBuf>,
|
||||||
pub client_certificate: Option<PathBuf>,
|
pub client_certificate: Option<AbsolutePathBuf>,
|
||||||
pub client_private_key: Option<PathBuf>,
|
pub client_private_key: Option<AbsolutePathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which OTEL exporter to use.
|
/// Which OTEL exporter to use.
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ otel = ["opentelemetry", "opentelemetry_sdk", "opentelemetry-otlp", "tonic"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
codex-app-server-protocol = { workspace = true }
|
codex-app-server-protocol = { workspace = true }
|
||||||
|
codex-utils-absolute-path = { workspace = true }
|
||||||
codex-protocol = { workspace = true }
|
codex-protocol = { workspace = true }
|
||||||
eventsource-stream = { workspace = true }
|
eventsource-stream = { workspace = true }
|
||||||
opentelemetry = { workspace = true, features = ["logs"], optional = true }
|
opentelemetry = { workspace = true, features = ["logs"], optional = true }
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct OtelSettings {
|
pub struct OtelSettings {
|
||||||
pub environment: String,
|
pub environment: String,
|
||||||
@@ -20,9 +22,9 @@ pub enum OtelHttpProtocol {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct OtelTlsConfig {
|
pub struct OtelTlsConfig {
|
||||||
pub ca_certificate: Option<PathBuf>,
|
pub ca_certificate: Option<AbsolutePathBuf>,
|
||||||
pub client_certificate: Option<PathBuf>,
|
pub client_certificate: Option<AbsolutePathBuf>,
|
||||||
pub client_private_key: Option<PathBuf>,
|
pub client_private_key: Option<AbsolutePathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::config::OtelExporter;
|
|||||||
use crate::config::OtelHttpProtocol;
|
use crate::config::OtelHttpProtocol;
|
||||||
use crate::config::OtelSettings;
|
use crate::config::OtelSettings;
|
||||||
use crate::config::OtelTlsConfig;
|
use crate::config::OtelTlsConfig;
|
||||||
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use http::Uri;
|
use http::Uri;
|
||||||
use opentelemetry::KeyValue;
|
use opentelemetry::KeyValue;
|
||||||
use opentelemetry_otlp::LogExporter;
|
use opentelemetry_otlp::LogExporter;
|
||||||
@@ -25,7 +26,6 @@ use std::error::Error;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::io::{self};
|
use std::io::{self};
|
||||||
use std::path::Path;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tonic::metadata::MetadataMap;
|
use tonic::metadata::MetadataMap;
|
||||||
@@ -85,12 +85,7 @@ impl OtelProvider {
|
|||||||
.assume_http2(true);
|
.assume_http2(true);
|
||||||
|
|
||||||
let tls_config = match tls.as_ref() {
|
let tls_config = match tls.as_ref() {
|
||||||
Some(tls) => build_grpc_tls_config(
|
Some(tls) => build_grpc_tls_config(endpoint, base_tls_config, tls)?,
|
||||||
endpoint,
|
|
||||||
base_tls_config,
|
|
||||||
tls,
|
|
||||||
settings.codex_home.as_path(),
|
|
||||||
)?,
|
|
||||||
None => base_tls_config,
|
None => base_tls_config,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,7 +118,7 @@ impl OtelProvider {
|
|||||||
.with_headers(headers.clone());
|
.with_headers(headers.clone());
|
||||||
|
|
||||||
if let Some(tls) = tls.as_ref() {
|
if let Some(tls) = tls.as_ref() {
|
||||||
let client = build_http_client(tls, settings.codex_home.as_path())?;
|
let client = build_http_client(tls)?;
|
||||||
exporter_builder = exporter_builder.with_http_client(client);
|
exporter_builder = exporter_builder.with_http_client(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +144,6 @@ fn build_grpc_tls_config(
|
|||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
tls_config: ClientTlsConfig,
|
tls_config: ClientTlsConfig,
|
||||||
tls: &OtelTlsConfig,
|
tls: &OtelTlsConfig,
|
||||||
codex_home: &Path,
|
|
||||||
) -> Result<ClientTlsConfig, Box<dyn Error>> {
|
) -> Result<ClientTlsConfig, Box<dyn Error>> {
|
||||||
let uri: Uri = endpoint.parse()?;
|
let uri: Uri = endpoint.parse()?;
|
||||||
let host = uri.host().ok_or_else(|| {
|
let host = uri.host().ok_or_else(|| {
|
||||||
@@ -161,14 +155,14 @@ fn build_grpc_tls_config(
|
|||||||
let mut config = tls_config.domain_name(host.to_owned());
|
let mut config = tls_config.domain_name(host.to_owned());
|
||||||
|
|
||||||
if let Some(path) = tls.ca_certificate.as_ref() {
|
if let Some(path) = tls.ca_certificate.as_ref() {
|
||||||
let (pem, _) = read_bytes(codex_home, path)?;
|
let (pem, _) = read_bytes(path)?;
|
||||||
config = config.ca_certificate(TonicCertificate::from_pem(pem));
|
config = config.ca_certificate(TonicCertificate::from_pem(pem));
|
||||||
}
|
}
|
||||||
|
|
||||||
match (&tls.client_certificate, &tls.client_private_key) {
|
match (&tls.client_certificate, &tls.client_private_key) {
|
||||||
(Some(cert_path), Some(key_path)) => {
|
(Some(cert_path), Some(key_path)) => {
|
||||||
let (cert_pem, _) = read_bytes(codex_home, cert_path)?;
|
let (cert_pem, _) = read_bytes(cert_path)?;
|
||||||
let (key_pem, _) = read_bytes(codex_home, key_path)?;
|
let (key_pem, _) = read_bytes(key_path)?;
|
||||||
config = config.identity(TonicIdentity::from_pem(cert_pem, key_pem));
|
config = config.identity(TonicIdentity::from_pem(cert_pem, key_pem));
|
||||||
}
|
}
|
||||||
(Some(_), None) | (None, Some(_)) => {
|
(Some(_), None) | (None, Some(_)) => {
|
||||||
@@ -188,24 +182,20 @@ fn build_grpc_tls_config(
|
|||||||
/// `opentelemetry_sdk` `BatchLogProcessor` spawns a dedicated OS thread that uses
|
/// `opentelemetry_sdk` `BatchLogProcessor` spawns a dedicated OS thread that uses
|
||||||
/// `futures_executor::block_on()` rather than tokio. When the async reqwest client's
|
/// `futures_executor::block_on()` rather than tokio. When the async reqwest client's
|
||||||
/// timeout calls `tokio::time::sleep()`, it panics with "no reactor running".
|
/// timeout calls `tokio::time::sleep()`, it panics with "no reactor running".
|
||||||
fn build_http_client(
|
fn build_http_client(tls: &OtelTlsConfig) -> Result<reqwest::blocking::Client, Box<dyn Error>> {
|
||||||
tls: &OtelTlsConfig,
|
|
||||||
codex_home: &Path,
|
|
||||||
) -> Result<reqwest::blocking::Client, Box<dyn Error>> {
|
|
||||||
// Wrap in block_in_place because reqwest::blocking::Client creates its own
|
// Wrap in block_in_place because reqwest::blocking::Client creates its own
|
||||||
// internal tokio runtime, which would panic if built directly from an async context.
|
// internal tokio runtime, which would panic if built directly from an async context.
|
||||||
tokio::task::block_in_place(|| build_http_client_inner(tls, codex_home))
|
tokio::task::block_in_place(|| build_http_client_inner(tls))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_http_client_inner(
|
fn build_http_client_inner(
|
||||||
tls: &OtelTlsConfig,
|
tls: &OtelTlsConfig,
|
||||||
codex_home: &Path,
|
|
||||||
) -> Result<reqwest::blocking::Client, Box<dyn Error>> {
|
) -> Result<reqwest::blocking::Client, Box<dyn Error>> {
|
||||||
let mut builder = reqwest::blocking::Client::builder()
|
let mut builder = reqwest::blocking::Client::builder()
|
||||||
.timeout(resolve_otlp_timeout(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT));
|
.timeout(resolve_otlp_timeout(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT));
|
||||||
|
|
||||||
if let Some(path) = tls.ca_certificate.as_ref() {
|
if let Some(path) = tls.ca_certificate.as_ref() {
|
||||||
let (pem, location) = read_bytes(codex_home, path)?;
|
let (pem, location) = read_bytes(path)?;
|
||||||
let certificate = ReqwestCertificate::from_pem(pem.as_slice()).map_err(|error| {
|
let certificate = ReqwestCertificate::from_pem(pem.as_slice()).map_err(|error| {
|
||||||
config_error(format!(
|
config_error(format!(
|
||||||
"failed to parse certificate {}: {error}",
|
"failed to parse certificate {}: {error}",
|
||||||
@@ -220,8 +210,8 @@ fn build_http_client_inner(
|
|||||||
|
|
||||||
match (&tls.client_certificate, &tls.client_private_key) {
|
match (&tls.client_certificate, &tls.client_private_key) {
|
||||||
(Some(cert_path), Some(key_path)) => {
|
(Some(cert_path), Some(key_path)) => {
|
||||||
let (mut cert_pem, cert_location) = read_bytes(codex_home, cert_path)?;
|
let (mut cert_pem, cert_location) = read_bytes(cert_path)?;
|
||||||
let (key_pem, key_location) = read_bytes(codex_home, key_path)?;
|
let (key_pem, key_location) = read_bytes(key_path)?;
|
||||||
cert_pem.extend_from_slice(key_pem.as_slice());
|
cert_pem.extend_from_slice(key_pem.as_slice());
|
||||||
let identity = ReqwestIdentity::from_pem(cert_pem.as_slice()).map_err(|error| {
|
let identity = ReqwestIdentity::from_pem(cert_pem.as_slice()).map_err(|error| {
|
||||||
config_error(format!(
|
config_error(format!(
|
||||||
@@ -264,25 +254,16 @@ fn read_timeout_env(var: &str) -> Option<Duration> {
|
|||||||
Some(Duration::from_millis(parsed as u64))
|
Some(Duration::from_millis(parsed as u64))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_bytes(base: &Path, provided: &PathBuf) -> Result<(Vec<u8>, PathBuf), Box<dyn Error>> {
|
fn read_bytes(path: &AbsolutePathBuf) -> Result<(Vec<u8>, PathBuf), Box<dyn Error>> {
|
||||||
let resolved = resolve_config_path(base, provided);
|
match fs::read(path) {
|
||||||
match fs::read(&resolved) {
|
Ok(bytes) => Ok((bytes, path.to_path_buf())),
|
||||||
Ok(bytes) => Ok((bytes, resolved)),
|
|
||||||
Err(error) => Err(Box::new(io::Error::new(
|
Err(error) => Err(Box::new(io::Error::new(
|
||||||
error.kind(),
|
error.kind(),
|
||||||
format!("failed to read {}: {error}", resolved.display()),
|
format!("failed to read {}: {error}", path.display()),
|
||||||
))),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_config_path(base: &Path, provided: &PathBuf) -> PathBuf {
|
|
||||||
if provided.is_absolute() {
|
|
||||||
provided.clone()
|
|
||||||
} else {
|
|
||||||
base.join(provided)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_error(message: impl Into<String>) -> Box<dyn Error> {
|
fn config_error(message: impl Into<String>) -> Box<dyn Error> {
|
||||||
Box::new(io::Error::new(ErrorKind::InvalidData, message.into()))
|
Box::new(io::Error::new(ErrorKind::InvalidData, message.into()))
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
[package]
|
||||||
|
name = "codex-utils-absolute-path"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
path-absolutize = { workspace = true }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
tempfile = { workspace = true }
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
use path_absolutize::Absolutize;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Deserializer;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde::de::Error as SerdeError;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::path::Display;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// A path that is guaranteed to be absolute and normalized (though it is not
|
||||||
|
/// guaranteed to be canonicalized or exist on the filesystem).
|
||||||
|
///
|
||||||
|
/// IMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set
|
||||||
|
/// using `AbsolutePathBufGuard::new(base_path)`. If no base path is set, the
|
||||||
|
/// deserialization will fail unless the path being deserialized is already
|
||||||
|
/// absolute.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct AbsolutePathBuf(PathBuf);
|
||||||
|
|
||||||
|
impl AbsolutePathBuf {
|
||||||
|
pub fn resolve_path_against_base<P, B>(path: P, base_path: B) -> std::io::Result<Self>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
B: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let absolute_path = path.as_ref().absolutize_from(base_path.as_ref())?;
|
||||||
|
Ok(Self(absolute_path.into_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_absolute_path<P>(path: P) -> std::io::Result<Self>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let absolute_path = path.as_ref().absolutize()?;
|
||||||
|
Ok(Self(absolute_path.into_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_path(&self) -> &Path {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_path_buf(self) -> PathBuf {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_path_buf(&self) -> PathBuf {
|
||||||
|
self.0.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display(&self) -> Display<'_> {
|
||||||
|
self.0.display()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static ABSOLUTE_PATH_BASE: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AbsolutePathBufGuard;
|
||||||
|
|
||||||
|
impl AbsolutePathBufGuard {
|
||||||
|
pub fn new(base_path: &Path) -> Self {
|
||||||
|
ABSOLUTE_PATH_BASE.with(|cell| {
|
||||||
|
*cell.borrow_mut() = Some(base_path.to_path_buf());
|
||||||
|
});
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for AbsolutePathBufGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
ABSOLUTE_PATH_BASE.with(|cell| {
|
||||||
|
*cell.borrow_mut() = None;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for AbsolutePathBuf {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let path = PathBuf::deserialize(deserializer)?;
|
||||||
|
ABSOLUTE_PATH_BASE.with(|cell| match cell.borrow().as_deref() {
|
||||||
|
Some(base) => {
|
||||||
|
Ok(Self::resolve_path_against_base(path, base).map_err(SerdeError::custom)?)
|
||||||
|
}
|
||||||
|
None if path.is_absolute() => {
|
||||||
|
Self::from_absolute_path(path).map_err(SerdeError::custom)
|
||||||
|
}
|
||||||
|
None => Err(SerdeError::custom(
|
||||||
|
"AbsolutePathBuf deserialized without a base path",
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<Path> for AbsolutePathBuf {
|
||||||
|
fn as_ref(&self) -> &Path {
|
||||||
|
self.as_path()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AbsolutePathBuf> for PathBuf {
|
||||||
|
fn from(path: AbsolutePathBuf) -> Self {
|
||||||
|
path.into_path_buf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_with_absolute_path_ignores_base_path() {
|
||||||
|
let base_dir = tempdir().expect("base dir");
|
||||||
|
let absolute_dir = tempdir().expect("absolute dir");
|
||||||
|
let base_path = base_dir.path();
|
||||||
|
let absolute_path = absolute_dir.path().join("file.txt");
|
||||||
|
let abs_path_buf =
|
||||||
|
AbsolutePathBuf::resolve_path_against_base(absolute_path.clone(), base_path)
|
||||||
|
.expect("failed to create");
|
||||||
|
assert_eq!(abs_path_buf.as_path(), absolute_path.as_path());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_path_is_resolved_against_base_path() {
|
||||||
|
let temp_dir = tempdir().expect("base dir");
|
||||||
|
let base_dir = temp_dir.path();
|
||||||
|
let abs_path_buf = AbsolutePathBuf::resolve_path_against_base("file.txt", base_dir)
|
||||||
|
.expect("failed to create");
|
||||||
|
assert_eq!(abs_path_buf.as_path(), base_dir.join("file.txt").as_path());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn guard_used_in_deserialization() {
|
||||||
|
let temp_dir = tempdir().expect("base dir");
|
||||||
|
let base_dir = temp_dir.path();
|
||||||
|
let relative_path = "subdir/file.txt";
|
||||||
|
let abs_path_buf = {
|
||||||
|
let _guard = AbsolutePathBufGuard::new(base_dir);
|
||||||
|
serde_json::from_str::<AbsolutePathBuf>(&format!(r#""{relative_path}""#))
|
||||||
|
.expect("failed to deserialize")
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
abs_path_buf.as_path(),
|
||||||
|
base_dir.join(relative_path).as_path()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user