mirror of
https://github.com/openai/codex.git
synced 2026-05-02 02:17:22 +00:00
## Problem being solved
- We need a single, reliable way to mark app-server API surface as
experimental so that:
1. the runtime can reject experimental usage unless the client opts in
2. generated TS/JSON schemas can exclude experimental methods/fields for
stable clients.
Right now that’s easy to drift or miss when done ad-hoc.
## How to declare experimental methods and fields
- **Experimental method**: add `#[experimental("method/name")]` to the
`ClientRequest` variant in `client_request_definitions!`.
- **Experimental field**: on the params struct, derive `ExperimentalApi`
and annotate the field with `#[experimental("method/name.field")]` + set
`inspect_params: true` for the method variant so
`ClientRequest::experimental_reason()` inspects params for experimental
fields.
## How the macro solves it
- The new derive macro lives in
`codex-rs/codex-experimental-api-macros/src/lib.rs` and is used via
`#[derive(ExperimentalApi)]` plus `#[experimental("reason")]`
attributes.
- **Structs**:
- Generates `ExperimentalApi::experimental_reason(&self)` that checks
only annotated fields.
- The “presence” check is type-aware:
- `Option<T>`: `is_some_and(...)` recursively checks inner.
- `Vec`/`HashMap`/`BTreeMap`: must be non-empty.
- `bool`: must be `true`.
- Other types: considered present (returns `true`).
- Registers each experimental field in an `inventory` with `(type_name,
serialized field name, reason)` and exposes `EXPERIMENTAL_FIELDS` for
that type. Field names are converted from `snake_case` to `camelCase`
for schema/TS filtering.
- **Enums**:
- Generates an exhaustive `match` returning `Some(reason)` for annotated
variants and `None` otherwise (no wildcard arm).
- **Wiring**:
- Runtime gating uses `ExperimentalApi::experimental_reason()` in
`codex-rs/app-server/src/message_processor.rs` to reject requests unless
`InitializeParams.capabilities.experimental_api == true`.
- Schema/TS export filters use the inventory list and
`EXPERIMENTAL_CLIENT_METHODS` from `client_request_definitions!` to
strip experimental methods/fields when `experimental_api` is false.
227 lines
7.6 KiB
Rust
227 lines
7.6 KiB
Rust
use std::ffi::OsString;
|
|
use std::io;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
pub use runfiles;
|
|
|
|
/// Bazel sets this when runfiles directories are disabled, which we do on all platforms for consistency.
|
|
const RUNFILES_MANIFEST_ONLY_ENV: &str = "RUNFILES_MANIFEST_ONLY";
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum CargoBinError {
|
|
#[error("failed to read current exe")]
|
|
CurrentExe {
|
|
#[source]
|
|
source: std::io::Error,
|
|
},
|
|
#[error("failed to read current directory")]
|
|
CurrentDir {
|
|
#[source]
|
|
source: std::io::Error,
|
|
},
|
|
#[error("CARGO_BIN_EXE env var {key} resolved to {path:?}, but it does not exist")]
|
|
ResolvedPathDoesNotExist { key: String, path: PathBuf },
|
|
#[error("could not locate binary {name:?}; tried env vars {env_keys:?}; {fallback}")]
|
|
NotFound {
|
|
name: String,
|
|
env_keys: Vec<String>,
|
|
fallback: String,
|
|
},
|
|
}
|
|
|
|
/// Returns an absolute path to a binary target built for the current test run.
|
|
///
|
|
/// In `cargo test`, `CARGO_BIN_EXE_*` env vars are absolute.
|
|
/// In `bazel test`, `CARGO_BIN_EXE_*` env vars are rlocationpaths, intended to be consumed by `rlocation`.
|
|
/// This helper allows callers to transparently support both.
|
|
#[allow(deprecated)]
|
|
pub fn cargo_bin(name: &str) -> Result<PathBuf, CargoBinError> {
|
|
let env_keys = cargo_bin_env_keys(name);
|
|
for key in &env_keys {
|
|
if let Some(value) = std::env::var_os(key) {
|
|
return resolve_bin_from_env(key, value);
|
|
}
|
|
}
|
|
match assert_cmd::Command::cargo_bin(name) {
|
|
Ok(cmd) => {
|
|
let mut path = PathBuf::from(cmd.get_program());
|
|
if !path.is_absolute() {
|
|
path = std::env::current_dir()
|
|
.map_err(|source| CargoBinError::CurrentDir { source })?
|
|
.join(path);
|
|
}
|
|
if path.exists() {
|
|
Ok(path)
|
|
} else {
|
|
Err(CargoBinError::ResolvedPathDoesNotExist {
|
|
key: "assert_cmd::Command::cargo_bin".to_owned(),
|
|
path,
|
|
})
|
|
}
|
|
}
|
|
Err(err) => Err(CargoBinError::NotFound {
|
|
name: name.to_owned(),
|
|
env_keys,
|
|
fallback: format!("assert_cmd fallback failed: {err}"),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn cargo_bin_env_keys(name: &str) -> Vec<String> {
|
|
let mut keys = Vec::with_capacity(2);
|
|
keys.push(format!("CARGO_BIN_EXE_{name}"));
|
|
|
|
// Cargo replaces dashes in target names when exporting env vars.
|
|
let underscore_name = name.replace('-', "_");
|
|
if underscore_name != name {
|
|
keys.push(format!("CARGO_BIN_EXE_{underscore_name}"));
|
|
}
|
|
|
|
keys
|
|
}
|
|
|
|
pub fn runfiles_available() -> bool {
|
|
std::env::var_os(RUNFILES_MANIFEST_ONLY_ENV).is_some()
|
|
}
|
|
|
|
fn resolve_bin_from_env(key: &str, value: OsString) -> Result<PathBuf, CargoBinError> {
|
|
let raw = PathBuf::from(&value);
|
|
if runfiles_available() {
|
|
let runfiles = runfiles::Runfiles::create().map_err(|err| CargoBinError::CurrentExe {
|
|
source: std::io::Error::other(err),
|
|
})?;
|
|
if let Some(resolved) = runfiles::rlocation!(runfiles, &raw)
|
|
&& resolved.exists()
|
|
{
|
|
return Ok(resolved);
|
|
}
|
|
} else if raw.is_absolute() && raw.exists() {
|
|
return Ok(raw);
|
|
}
|
|
|
|
Err(CargoBinError::ResolvedPathDoesNotExist {
|
|
key: key.to_owned(),
|
|
path: raw,
|
|
})
|
|
}
|
|
|
|
/// Macro that derives the path to a test resource at runtime, the value of
|
|
/// which depends on whether Cargo or Bazel is being used to build and run a
|
|
/// test. Note the return value may be a relative or absolute path.
|
|
/// (Incidentally, this is a macro rather than a function because it reads
|
|
/// compile-time environment variables that need to be captured at the call
|
|
/// site.)
|
|
///
|
|
/// This is expected to be used exclusively in test code because Codex CLI is a
|
|
/// standalone binary with no packaged resources.
|
|
#[macro_export]
|
|
macro_rules! find_resource {
|
|
($resource:expr) => {{
|
|
let resource = std::path::Path::new(&$resource);
|
|
if $crate::runfiles_available() {
|
|
// When this code is built and run with Bazel:
|
|
// - we inject `BAZEL_PACKAGE` as a compile-time environment variable
|
|
// that points to native.package_name()
|
|
// - at runtime, Bazel will set runfiles-related env vars
|
|
$crate::resolve_bazel_runfile(option_env!("BAZEL_PACKAGE"), resource)
|
|
} else {
|
|
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
|
Ok(manifest_dir.join(resource))
|
|
}
|
|
}};
|
|
}
|
|
|
|
pub fn resolve_bazel_runfile(
|
|
bazel_package: Option<&str>,
|
|
resource: &Path,
|
|
) -> std::io::Result<PathBuf> {
|
|
let runfiles = runfiles::Runfiles::create()
|
|
.map_err(|err| std::io::Error::other(format!("failed to create runfiles: {err}")))?;
|
|
let runfile_path = match bazel_package {
|
|
Some(bazel_package) => PathBuf::from("_main").join(bazel_package).join(resource),
|
|
None => {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::NotFound,
|
|
"BAZEL_PACKAGE was not set at compile time",
|
|
));
|
|
}
|
|
};
|
|
let runfile_path = normalize_runfile_path(&runfile_path);
|
|
if let Some(resolved) = runfiles::rlocation!(runfiles, &runfile_path)
|
|
&& resolved.exists()
|
|
{
|
|
return Ok(resolved);
|
|
}
|
|
let runfile_path_display = runfile_path.display();
|
|
Err(std::io::Error::new(
|
|
std::io::ErrorKind::NotFound,
|
|
format!("runfile does not exist at: {runfile_path_display}"),
|
|
))
|
|
}
|
|
|
|
pub fn resolve_cargo_runfile(resource: &Path) -> std::io::Result<PathBuf> {
|
|
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
Ok(manifest_dir.join(resource))
|
|
}
|
|
|
|
pub fn repo_root() -> io::Result<PathBuf> {
|
|
let marker = if runfiles_available() {
|
|
let runfiles = runfiles::Runfiles::create()
|
|
.map_err(|err| io::Error::other(format!("failed to create runfiles: {err}")))?;
|
|
let marker_path = option_env!("CODEX_REPO_ROOT_MARKER")
|
|
.map(PathBuf::from)
|
|
.ok_or_else(|| {
|
|
io::Error::new(
|
|
io::ErrorKind::NotFound,
|
|
"CODEX_REPO_ROOT_MARKER was not set at compile time",
|
|
)
|
|
})?;
|
|
runfiles::rlocation!(runfiles, &marker_path).ok_or_else(|| {
|
|
io::Error::new(
|
|
io::ErrorKind::NotFound,
|
|
"repo_root.marker not available in runfiles",
|
|
)
|
|
})?
|
|
} else {
|
|
resolve_cargo_runfile(Path::new("repo_root.marker"))?
|
|
};
|
|
let mut root = marker;
|
|
for _ in 0..4 {
|
|
root = root
|
|
.parent()
|
|
.ok_or_else(|| {
|
|
io::Error::new(
|
|
io::ErrorKind::NotFound,
|
|
"repo_root.marker did not have expected parent depth",
|
|
)
|
|
})?
|
|
.to_path_buf();
|
|
}
|
|
Ok(root)
|
|
}
|
|
|
|
fn normalize_runfile_path(path: &Path) -> PathBuf {
|
|
let mut components = Vec::new();
|
|
for component in path.components() {
|
|
match component {
|
|
std::path::Component::CurDir => {}
|
|
std::path::Component::ParentDir => {
|
|
if matches!(components.last(), Some(std::path::Component::Normal(_))) {
|
|
components.pop();
|
|
} else {
|
|
components.push(component);
|
|
}
|
|
}
|
|
_ => components.push(component),
|
|
}
|
|
}
|
|
|
|
components
|
|
.into_iter()
|
|
.fold(PathBuf::new(), |mut acc, component| {
|
|
acc.push(component.as_os_str());
|
|
acc
|
|
})
|
|
}
|