mirror of
https://github.com/openai/codex.git
synced 2026-04-07 16:11:41 +03:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8719ac1d6 | ||
|
|
4e27a87ec6 |
104
.github/scripts/run-bazel-ci.sh
vendored
104
.github/scripts/run-bazel-ci.sh
vendored
@@ -5,6 +5,8 @@ set -euo pipefail
|
||||
print_failed_bazel_test_logs=0
|
||||
use_node_test_env=0
|
||||
remote_download_toplevel=0
|
||||
use_ci_config=0
|
||||
ci_config_override=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -20,6 +22,24 @@ while [[ $# -gt 0 ]]; do
|
||||
remote_download_toplevel=1
|
||||
shift
|
||||
;;
|
||||
--use-ci-config)
|
||||
use_ci_config=1
|
||||
shift
|
||||
;;
|
||||
--ci-config=*)
|
||||
use_ci_config=1
|
||||
ci_config_override="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--ci-config)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Expected a value after --ci-config" >&2
|
||||
exit 1
|
||||
fi
|
||||
use_ci_config=1
|
||||
ci_config_override="$2"
|
||||
shift 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
@@ -32,7 +52,7 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "Usage: $0 [--print-failed-test-logs] [--use-node-test-env] [--remote-download-toplevel] -- <bazel args> -- <targets>" >&2
|
||||
echo "Usage: $0 [--print-failed-test-logs] [--use-node-test-env] [--remote-download-toplevel] [--use-ci-config] [--ci-config=<config>] -- <bazel args> -- <targets>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -41,8 +61,23 @@ if [[ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]]; then
|
||||
bazel_startup_args+=("--output_user_root=${BAZEL_OUTPUT_USER_ROOT}")
|
||||
fi
|
||||
|
||||
runner_os="${RUNNER_OS:-}"
|
||||
if [[ -z "${runner_os}" ]]; then
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
runner_os=macOS
|
||||
;;
|
||||
Linux)
|
||||
runner_os=Linux
|
||||
;;
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
runner_os=Windows
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
ci_config=ci-linux
|
||||
case "${RUNNER_OS:-}" in
|
||||
case "${runner_os}" in
|
||||
macOS)
|
||||
ci_config=ci-macos
|
||||
;;
|
||||
@@ -51,6 +86,10 @@ case "${RUNNER_OS:-}" in
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -n "${ci_config_override}" ]]; then
|
||||
ci_config="${ci_config_override}"
|
||||
fi
|
||||
|
||||
print_bazel_test_log_tails() {
|
||||
local console_log="$1"
|
||||
local testlogs_dir
|
||||
@@ -112,7 +151,7 @@ if [[ ${#bazel_args[@]} -eq 0 || ${#bazel_targets[@]} -eq 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $use_node_test_env -eq 1 && "${RUNNER_OS:-}" != "Windows" ]]; then
|
||||
if [[ $use_node_test_env -eq 1 && "${runner_os}" != "Windows" ]]; then
|
||||
# Bazel test sandboxes on macOS may resolve an older Homebrew `node`
|
||||
# before the `actions/setup-node` runtime on PATH.
|
||||
node_bin="$(which node)"
|
||||
@@ -134,6 +173,11 @@ if (( ${#bazel_startup_args[@]} > 0 )); then
|
||||
bazel_cmd+=("${bazel_startup_args[@]}")
|
||||
fi
|
||||
|
||||
ci_config_args=()
|
||||
if [[ -n "${BUILDBUDDY_API_KEY:-}" || $use_ci_config -eq 1 ]]; then
|
||||
ci_config_args+=("--config=${ci_config}")
|
||||
fi
|
||||
|
||||
if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
|
||||
echo "BuildBuddy API key is available; using remote Bazel configuration."
|
||||
# Work around Bazel 9 remote repo contents cache / overlay materialization failures
|
||||
@@ -142,7 +186,7 @@ if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
|
||||
# remote execution/cache; this only disables the startup-level repo contents cache.
|
||||
bazel_run_args=(
|
||||
"${bazel_args[@]}"
|
||||
"--config=${ci_config}"
|
||||
"${ci_config_args[@]}"
|
||||
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
|
||||
)
|
||||
if (( ${#post_config_bazel_args[@]} > 0 )); then
|
||||
@@ -158,28 +202,36 @@ if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
|
||||
bazel_status=${PIPESTATUS[0]}
|
||||
set -e
|
||||
else
|
||||
echo "BuildBuddy API key is not available; using local Bazel configuration."
|
||||
# Keep fork/community PRs on Bazel but disable remote services that are
|
||||
# configured in .bazelrc and require auth.
|
||||
#
|
||||
# Flag docs:
|
||||
# - Command-line reference: https://bazel.build/reference/command-line-reference
|
||||
# - Remote caching overview: https://bazel.build/remote/caching
|
||||
# - Remote execution overview: https://bazel.build/remote/rbe
|
||||
# - Build Event Protocol overview: https://bazel.build/remote/bep
|
||||
#
|
||||
# --noexperimental_remote_repo_contents_cache:
|
||||
# disable remote repo contents cache enabled in .bazelrc startup options.
|
||||
# https://bazel.build/reference/command-line-reference#startup_options-flag--experimental_remote_repo_contents_cache
|
||||
# --remote_cache= and --remote_executor=:
|
||||
# clear remote cache/execution endpoints configured in .bazelrc.
|
||||
# https://bazel.build/reference/command-line-reference#common_options-flag--remote_cache
|
||||
# https://bazel.build/reference/command-line-reference#common_options-flag--remote_executor
|
||||
bazel_run_args=(
|
||||
"${bazel_args[@]}"
|
||||
--remote_cache=
|
||||
--remote_executor=
|
||||
)
|
||||
bazel_run_args=("${bazel_args[@]}" "${ci_config_args[@]}")
|
||||
|
||||
if [[ -n "${GITHUB_ACTIONS:-}" ]]; then
|
||||
echo "BuildBuddy API key is not available in GitHub Actions; disabling remote Bazel services."
|
||||
# Keep fork/community PRs on Bazel but disable remote services that are
|
||||
# configured in .bazelrc and require auth.
|
||||
#
|
||||
# Flag docs:
|
||||
# - Command-line reference: https://bazel.build/reference/command-line-reference
|
||||
# - Remote caching overview: https://bazel.build/remote/caching
|
||||
# - Remote execution overview: https://bazel.build/remote/rbe
|
||||
# - Build Event Protocol overview: https://bazel.build/remote/bep
|
||||
#
|
||||
# --noexperimental_remote_repo_contents_cache:
|
||||
# disable remote repo contents cache enabled in .bazelrc startup options.
|
||||
# https://bazel.build/reference/command-line-reference#startup_options-flag--experimental_remote_repo_contents_cache
|
||||
# --remote_cache= and --remote_executor=:
|
||||
# clear remote cache/execution endpoints configured in .bazelrc.
|
||||
# https://bazel.build/reference/command-line-reference#common_options-flag--remote_cache
|
||||
# https://bazel.build/reference/command-line-reference#common_options-flag--remote_executor
|
||||
bazel_run_args+=(
|
||||
--remote_cache=
|
||||
--remote_executor=
|
||||
)
|
||||
elif [[ $use_ci_config -eq 1 ]]; then
|
||||
echo "BuildBuddy API key env var is not available; using ${ci_config} with remote settings from local Bazel config."
|
||||
else
|
||||
echo "BuildBuddy API key env var is not available; preserving Bazel remote settings from local Bazel config."
|
||||
fi
|
||||
|
||||
if (( ${#post_config_bazel_args[@]} > 0 )); then
|
||||
bazel_run_args+=("${post_config_bazel_args[@]}")
|
||||
fi
|
||||
|
||||
@@ -472,15 +472,10 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
|
||||
first_file.display(),
|
||||
second_file.display()
|
||||
);
|
||||
// Login shells can emit an extra approval for system startup helpers
|
||||
// (for example `/usr/libexec/path_helper -s` on macOS) before the target
|
||||
// `rm` subcommands. Give the command enough budget to exercise the full
|
||||
// approval sequence on slower CI shards.
|
||||
let tool_timeout_ms = 15_000;
|
||||
let tool_call_arguments = serde_json::to_string(&serde_json::json!({
|
||||
"command": shell_command,
|
||||
"workdir": serde_json::Value::Null,
|
||||
"timeout_ms": tool_timeout_ms
|
||||
"timeout_ms": 5000
|
||||
}))?;
|
||||
let response = responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
|
||||
@@ -72,6 +72,7 @@ use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_tools::create_tools_json_for_responses_api;
|
||||
use eventsource_stream::Event;
|
||||
use eventsource_stream::EventStreamError;
|
||||
use futures::StreamExt;
|
||||
@@ -107,7 +108,6 @@ use crate::response_debug_context::extract_response_debug_context;
|
||||
use crate::response_debug_context::extract_response_debug_context_from_api_error;
|
||||
use crate::response_debug_context::telemetry_api_error_message;
|
||||
use crate::response_debug_context::telemetry_transport_error_message;
|
||||
use crate::tools::spec::create_tools_json_for_responses_api;
|
||||
use crate::util::FeedbackRequestTags;
|
||||
use crate::util::emit_feedback_auth_recovery_tags;
|
||||
use crate::util::emit_feedback_request_tags_with_auth_env;
|
||||
|
||||
@@ -24,6 +24,7 @@ use codex_hooks::HookToolInput;
|
||||
use codex_hooks::HookToolInputLocalShell;
|
||||
use codex_hooks::HookToolKind;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_tools::ConfiguredToolSpec;
|
||||
use codex_utils_readiness::Readiness;
|
||||
use serde_json::Value;
|
||||
use tracing::warn;
|
||||
@@ -436,21 +437,6 @@ impl ToolRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConfiguredToolSpec {
|
||||
pub spec: ToolSpec,
|
||||
pub supports_parallel_tool_calls: bool,
|
||||
}
|
||||
|
||||
impl ConfiguredToolSpec {
|
||||
pub fn new(spec: ToolSpec, supports_parallel_tool_calls: bool) -> Self {
|
||||
Self {
|
||||
spec,
|
||||
supports_parallel_tool_calls,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ToolRegistryBuilder {
|
||||
handlers: HashMap<String, Arc<dyn AnyToolHandler>>,
|
||||
specs: Vec<ConfiguredToolSpec>,
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::discoverable::DiscoverableTool;
|
||||
use crate::tools::registry::AnyToolResult;
|
||||
use crate::tools::registry::ConfiguredToolSpec;
|
||||
use crate::tools::registry::ToolRegistry;
|
||||
use crate::tools::spec::ToolsConfig;
|
||||
use crate::tools::spec::build_specs_with_discoverable_tools;
|
||||
@@ -18,6 +17,7 @@ use codex_protocol::models::LocalShellAction;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::models::SearchToolCallParams;
|
||||
use codex_protocol::models::ShellToolCallParams;
|
||||
use codex_tools::ConfiguredToolSpec;
|
||||
use rmcp::model::Tool;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
@@ -66,7 +66,7 @@ impl ToolRouter {
|
||||
specs
|
||||
.iter()
|
||||
.filter_map(|configured_tool| {
|
||||
if !codex_code_mode::is_code_mode_nested_tool(configured_tool.spec.name()) {
|
||||
if !codex_code_mode::is_code_mode_nested_tool(configured_tool.name()) {
|
||||
Some(configured_tool.spec.clone())
|
||||
} else {
|
||||
None
|
||||
@@ -101,7 +101,7 @@ impl ToolRouter {
|
||||
pub fn find_spec(&self, tool_name: &str) -> Option<ToolSpec> {
|
||||
self.specs
|
||||
.iter()
|
||||
.find(|config| config.spec.name() == tool_name)
|
||||
.find(|config| config.name() == tool_name)
|
||||
.map(|config| config.spec.clone())
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ impl ToolRouter {
|
||||
self.specs
|
||||
.iter()
|
||||
.filter(|config| config.supports_parallel_tool_calls)
|
||||
.any(|config| config.spec.name() == tool_name)
|
||||
.any(|config| config.name() == tool_name)
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all, err)]
|
||||
|
||||
@@ -2351,22 +2351,6 @@ pub(crate) struct ApplyPatchToolArgs {
|
||||
pub(crate) input: String,
|
||||
}
|
||||
|
||||
/// Returns JSON values that are compatible with Function Calling in the
|
||||
/// Responses API:
|
||||
/// https://platform.openai.com/docs/guides/function-calling?api-mode=responses
|
||||
pub fn create_tools_json_for_responses_api(
|
||||
tools: &[ToolSpec],
|
||||
) -> crate::error::Result<Vec<serde_json::Value>> {
|
||||
let mut tools_json = Vec::new();
|
||||
|
||||
for tool in tools {
|
||||
let json = serde_json::to_value(tool)?;
|
||||
tools_json.push(json);
|
||||
}
|
||||
|
||||
Ok(tools_json)
|
||||
}
|
||||
|
||||
fn push_tool_spec(
|
||||
builder: &mut ToolRegistryBuilder,
|
||||
spec: ToolSpec,
|
||||
|
||||
@@ -4,13 +4,13 @@ use crate::models_manager::model_info::with_config_overrides;
|
||||
use crate::shell::Shell;
|
||||
use crate::shell::ShellType;
|
||||
use crate::tools::ToolRouter;
|
||||
use crate::tools::registry::ConfiguredToolSpec;
|
||||
use crate::tools::router::ToolRouterParams;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_protocol::openai_models::InputModality;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_tools::AdditionalProperties;
|
||||
use codex_tools::ConfiguredToolSpec;
|
||||
use codex_tools::FreeformTool;
|
||||
use codex_tools::ResponsesApiWebSearchFilters;
|
||||
use codex_tools::ResponsesApiWebSearchUserLocation;
|
||||
@@ -107,16 +107,12 @@ fn deferred_responses_api_tool_serializes_with_defer_loading() {
|
||||
);
|
||||
}
|
||||
|
||||
fn tool_name(tool: &ToolSpec) -> &str {
|
||||
tool.name()
|
||||
}
|
||||
|
||||
// Avoid order-based assertions; compare via set containment instead.
|
||||
fn assert_contains_tool_names(tools: &[ConfiguredToolSpec], expected_subset: &[&str]) {
|
||||
use std::collections::HashSet;
|
||||
let mut names = HashSet::new();
|
||||
let mut duplicates = Vec::new();
|
||||
for name in tools.iter().map(|t| tool_name(&t.spec)) {
|
||||
for name in tools.iter().map(ConfiguredToolSpec::name) {
|
||||
if !names.insert(name) {
|
||||
duplicates.push(name);
|
||||
}
|
||||
@@ -136,7 +132,7 @@ fn assert_contains_tool_names(tools: &[ConfiguredToolSpec], expected_subset: &[&
|
||||
fn assert_lacks_tool_name(tools: &[ConfiguredToolSpec], expected_absent: &str) {
|
||||
let names = tools
|
||||
.iter()
|
||||
.map(|tool| tool_name(&tool.spec))
|
||||
.map(ConfiguredToolSpec::name)
|
||||
.collect::<Vec<_>>();
|
||||
assert!(
|
||||
!names.contains(&expected_absent),
|
||||
@@ -157,7 +153,7 @@ fn shell_tool_name(config: &ToolsConfig) -> Option<&'static str> {
|
||||
fn find_tool<'a>(tools: &'a [ConfiguredToolSpec], expected_name: &str) -> &'a ConfiguredToolSpec {
|
||||
tools
|
||||
.iter()
|
||||
.find(|tool| tool_name(&tool.spec) == expected_name)
|
||||
.find(|tool| tool.name() == expected_name)
|
||||
.unwrap_or_else(|| panic!("expected tool {expected_name}"))
|
||||
}
|
||||
|
||||
@@ -289,7 +285,7 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
|
||||
let mut actual: BTreeMap<String, ToolSpec> = BTreeMap::from([]);
|
||||
let mut duplicate_names = Vec::new();
|
||||
for t in &tools {
|
||||
let name = tool_name(&t.spec).to_string();
|
||||
let name = t.name().to_string();
|
||||
if actual.insert(name.clone(), t.spec.clone()).is_some() {
|
||||
duplicate_names.push(name);
|
||||
}
|
||||
@@ -318,7 +314,7 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
|
||||
},
|
||||
create_view_image_tool(config.can_request_original_image_detail),
|
||||
] {
|
||||
expected.insert(tool_name(&spec).to_string(), spec);
|
||||
expected.insert(spec.name().to_string(), spec);
|
||||
}
|
||||
let collab_specs = if config.multi_agent_v2 {
|
||||
vec![
|
||||
@@ -336,16 +332,16 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
|
||||
]
|
||||
};
|
||||
for spec in collab_specs {
|
||||
expected.insert(tool_name(&spec).to_string(), spec);
|
||||
expected.insert(spec.name().to_string(), spec);
|
||||
}
|
||||
if !config.multi_agent_v2 {
|
||||
let spec = create_resume_agent_tool();
|
||||
expected.insert(tool_name(&spec).to_string(), spec);
|
||||
expected.insert(spec.name().to_string(), spec);
|
||||
}
|
||||
|
||||
if config.exec_permission_approvals_enabled {
|
||||
let spec = create_request_permissions_tool();
|
||||
expected.insert(tool_name(&spec).to_string(), spec);
|
||||
expected.insert(spec.name().to_string(), spec);
|
||||
}
|
||||
|
||||
// Exact name set match — this is the only test allowed to fail when tools change.
|
||||
@@ -1679,11 +1675,7 @@ fn test_test_model_info_includes_sync_tool() {
|
||||
)
|
||||
.build();
|
||||
|
||||
assert!(
|
||||
tools
|
||||
.iter()
|
||||
.any(|tool| tool_name(&tool.spec) == "test_sync_tool")
|
||||
);
|
||||
assert!(tools.iter().any(|tool| tool.name() == "test_sync_tool"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1817,7 +1809,7 @@ fn test_build_specs_mcp_tools_sorted_by_name() {
|
||||
// Only assert that the MCP tools themselves are sorted by fully-qualified name.
|
||||
let mcp_names: Vec<_> = tools
|
||||
.iter()
|
||||
.map(|t| tool_name(&t.spec).to_string())
|
||||
.map(|t| t.name().to_string())
|
||||
.filter(|n| n.starts_with("test_server/"))
|
||||
.collect();
|
||||
let expected = vec![
|
||||
@@ -2064,7 +2056,7 @@ fn tool_suggest_is_not_registered_without_feature_flag() {
|
||||
assert!(
|
||||
!tools
|
||||
.iter()
|
||||
.any(|tool| tool_name(&tool.spec) == TOOL_SUGGEST_TOOL_NAME)
|
||||
.any(|tool| tool.name() == TOOL_SUGGEST_TOOL_NAME)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2155,7 +2147,7 @@ fn tool_suggest_requires_apps_and_plugins_features() {
|
||||
assert!(
|
||||
!tools
|
||||
.iter()
|
||||
.any(|tool| tool_name(&tool.spec) == TOOL_SUGGEST_TOOL_NAME),
|
||||
.any(|tool| tool.name() == TOOL_SUGGEST_TOOL_NAME),
|
||||
"tool_suggest should be absent when {disabled_feature:?} is disabled"
|
||||
);
|
||||
}
|
||||
@@ -3106,38 +3098,3 @@ fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only(
|
||||
assert!(!description.contains("### `update_plan` (`update_plan`)"));
|
||||
assert!(!description.contains("### `view_image` (`view_image`)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chat_tools_include_top_level_name() {
|
||||
let properties =
|
||||
BTreeMap::from([("foo".to_string(), JsonSchema::String { description: None })]);
|
||||
let tools = vec![ToolSpec::Function(ResponsesApiTool {
|
||||
name: "demo".to_string(),
|
||||
description: "A demo tool".to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: None,
|
||||
additional_properties: None,
|
||||
},
|
||||
output_schema: None,
|
||||
})];
|
||||
|
||||
let responses_json = create_tools_json_for_responses_api(&tools).unwrap();
|
||||
assert_eq!(
|
||||
responses_json,
|
||||
vec![json!({
|
||||
"type": "function",
|
||||
"name": "demo",
|
||||
"description": "A demo tool",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"foo": { "type": "string" }
|
||||
},
|
||||
},
|
||||
})]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1099,12 +1099,18 @@ fn turn_items_for_thread(
|
||||
.map(|turn| turn.items.clone())
|
||||
}
|
||||
|
||||
fn resumable_thread_source_kinds() -> Vec<ThreadSourceKind> {
|
||||
fn all_thread_source_kinds() -> Vec<ThreadSourceKind> {
|
||||
vec![
|
||||
ThreadSourceKind::Cli,
|
||||
ThreadSourceKind::VsCode,
|
||||
ThreadSourceKind::Exec,
|
||||
ThreadSourceKind::AppServer,
|
||||
ThreadSourceKind::SubAgent,
|
||||
ThreadSourceKind::SubAgentReview,
|
||||
ThreadSourceKind::SubAgentCompact,
|
||||
ThreadSourceKind::SubAgentThreadSpawn,
|
||||
ThreadSourceKind::SubAgentOther,
|
||||
ThreadSourceKind::Unknown,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1163,7 +1169,7 @@ async fn resolve_resume_thread_id(
|
||||
limit: Some(100),
|
||||
sort_key: Some(ThreadSortKey::UpdatedAt),
|
||||
model_providers: model_providers.clone(),
|
||||
source_kinds: Some(resumable_thread_source_kinds()),
|
||||
source_kinds: Some(all_thread_source_kinds()),
|
||||
archived: Some(false),
|
||||
cwd: None,
|
||||
search_term: None,
|
||||
@@ -1203,7 +1209,7 @@ async fn resolve_resume_thread_id(
|
||||
limit: Some(100),
|
||||
sort_key: Some(ThreadSortKey::UpdatedAt),
|
||||
model_providers: model_providers.clone(),
|
||||
source_kinds: Some(resumable_thread_source_kinds()),
|
||||
source_kinds: Some(all_thread_source_kinds()),
|
||||
archived: Some(false),
|
||||
cwd: None,
|
||||
// Thread names are attached separately from rollout titles, so name
|
||||
@@ -1892,19 +1898,6 @@ mod tests {
|
||||
assert_eq!(resume_lookup_model_providers(&config, &named_args), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resumable_thread_source_kinds_exclude_internal_threads() {
|
||||
assert_eq!(
|
||||
resumable_thread_source_kinds(),
|
||||
vec![
|
||||
ThreadSourceKind::Cli,
|
||||
ThreadSourceKind::VsCode,
|
||||
ThreadSourceKind::Exec,
|
||||
ThreadSourceKind::AppServer,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_items_for_thread_returns_matching_turn_items() {
|
||||
let thread = AppServerThread {
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
use anyhow::Context;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::SessionMeta;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_utils_cargo_bin::find_resource;
|
||||
use core_test_support::test_codex_exec::test_codex_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::string::ToString;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
use uuid::Uuid;
|
||||
use walkdir::WalkDir;
|
||||
@@ -227,118 +220,6 @@ fn exec_resume_last_accepts_prompt_after_flag_in_json_mode() -> anyhow::Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_resume_last_ignores_newer_internal_thread() -> anyhow::Result<()> {
|
||||
let test = test_codex_exec();
|
||||
let fixture = exec_fixture()?;
|
||||
let repo_root = exec_repo_root()?;
|
||||
|
||||
let marker = format!("resume-last-visible-{}", Uuid::new_v4());
|
||||
let prompt = format!("echo {marker}");
|
||||
|
||||
test.cmd()
|
||||
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
||||
.env("OPENAI_BASE_URL", "http://unused.local")
|
||||
.arg("--skip-git-repo-check")
|
||||
.arg("-C")
|
||||
.arg(&repo_root)
|
||||
.arg(&prompt)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let sessions_dir = test.home_path().join("sessions");
|
||||
let path = find_session_file_containing_marker(&sessions_dir, &marker)
|
||||
.expect("no session file found after first run");
|
||||
|
||||
// `updated_at` is second-granularity, so make the injected internal thread
|
||||
// deterministically newer than the visible exec session.
|
||||
std::thread::sleep(Duration::from_millis(1100));
|
||||
|
||||
let internal_thread_id = Uuid::new_v4();
|
||||
let internal_rollout_path = test.home_path().join("sessions/2026/03/27").join(format!(
|
||||
"rollout-2026-03-27T00-00-00-{internal_thread_id}.jsonl"
|
||||
));
|
||||
std::fs::create_dir_all(
|
||||
internal_rollout_path
|
||||
.parent()
|
||||
.expect("internal rollout parent directory"),
|
||||
)?;
|
||||
|
||||
let internal_thread_id_str = internal_thread_id.to_string();
|
||||
let internal_payload = serde_json::to_value(SessionMetaLine {
|
||||
meta: SessionMeta {
|
||||
id: ThreadId::from_string(&internal_thread_id_str)?,
|
||||
forked_from_id: None,
|
||||
timestamp: "2026-03-27T00:00:00.000Z".to_string(),
|
||||
cwd: repo_root.clone(),
|
||||
originator: "codex".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: SessionSource::SubAgent(SubAgentSource::MemoryConsolidation),
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
},
|
||||
git: None,
|
||||
})?;
|
||||
let internal_lines = [
|
||||
json!({
|
||||
"timestamp": "2026-03-27T00:00:00.000Z",
|
||||
"type": "session_meta",
|
||||
"payload": internal_payload,
|
||||
})
|
||||
.to_string(),
|
||||
json!({
|
||||
"timestamp": "2026-03-27T00:00:00.000Z",
|
||||
"type": "response_item",
|
||||
"payload": {
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "internal memory sweep"}],
|
||||
},
|
||||
})
|
||||
.to_string(),
|
||||
json!({
|
||||
"timestamp": "2026-03-27T00:00:00.000Z",
|
||||
"type": "event_msg",
|
||||
"payload": {
|
||||
"type": "user_message",
|
||||
"message": "internal memory sweep",
|
||||
"kind": "plain",
|
||||
},
|
||||
})
|
||||
.to_string(),
|
||||
];
|
||||
std::fs::write(&internal_rollout_path, internal_lines.join("\n") + "\n")?;
|
||||
|
||||
let marker2 = format!("resume-last-visible-2-{}", Uuid::new_v4());
|
||||
let prompt2 = format!("echo {marker2}");
|
||||
|
||||
test.cmd()
|
||||
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
||||
.env("OPENAI_BASE_URL", "http://unused.local")
|
||||
.arg("--skip-git-repo-check")
|
||||
.arg("-C")
|
||||
.arg(&repo_root)
|
||||
.arg(&prompt2)
|
||||
.arg("resume")
|
||||
.arg("--last")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let resumed_path = find_session_file_containing_marker(&sessions_dir, &marker2)
|
||||
.expect("no resumed session file containing marker2");
|
||||
assert_eq!(
|
||||
resumed_path, path,
|
||||
"resume --last should ignore newer internal threads"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_resume_last_respects_cwd_filter_and_all_flag() -> anyhow::Result<()> {
|
||||
let test = test_codex_exec();
|
||||
|
||||
@@ -33,31 +33,17 @@ fn duplicate_fd_for_transfer(fd: impl AsFd, name: &str) -> anyhow::Result<OwnedF
|
||||
.with_context(|| format!("failed to duplicate {name} for escalation transfer"))
|
||||
}
|
||||
|
||||
async fn connect_escalation_stream(
|
||||
handshake_client: AsyncDatagramSocket,
|
||||
) -> anyhow::Result<(AsyncSocket, OwnedFd)> {
|
||||
let (server, client) = AsyncSocket::pair()?;
|
||||
let server_stream_guard: OwnedFd = server.into_inner().into();
|
||||
let transferred_server_stream =
|
||||
duplicate_fd_for_transfer(&server_stream_guard, "handshake stream")?;
|
||||
const HANDSHAKE_MESSAGE: [u8; 1] = [0];
|
||||
// Keep one local reference to the transferred stream alive until the server
|
||||
// answers the first request. On macOS, dropping the sender's last local copy
|
||||
// immediately after the datagram handshake can make the peer observe EOF
|
||||
// before the received fd is fully servicing the stream.
|
||||
handshake_client
|
||||
.send_with_fds(&HANDSHAKE_MESSAGE, &[transferred_server_stream])
|
||||
.await
|
||||
.context("failed to send handshake datagram")?;
|
||||
Ok((client, server_stream_guard))
|
||||
}
|
||||
|
||||
pub async fn run_shell_escalation_execve_wrapper(
|
||||
file: String,
|
||||
argv: Vec<String>,
|
||||
) -> anyhow::Result<i32> {
|
||||
let handshake_client = get_escalate_client()?;
|
||||
let (client, server_stream_guard) = connect_escalation_stream(handshake_client).await?;
|
||||
let (server, client) = AsyncSocket::pair()?;
|
||||
const HANDSHAKE_MESSAGE: [u8; 1] = [0];
|
||||
handshake_client
|
||||
.send_with_fds(&HANDSHAKE_MESSAGE, &[server.into_inner().into()])
|
||||
.await
|
||||
.context("failed to send handshake datagram")?;
|
||||
let env = std::env::vars()
|
||||
.filter(|(k, _)| !matches!(k.as_str(), ESCALATE_SOCKET_ENV_VAR | EXEC_WRAPPER_ENV_VAR))
|
||||
.collect();
|
||||
@@ -70,11 +56,6 @@ pub async fn run_shell_escalation_execve_wrapper(
|
||||
})
|
||||
.await
|
||||
.context("failed to send EscalateRequest")?;
|
||||
// Once the first request has been written into the stream, the local guard
|
||||
// is no longer needed to bridge the datagram handoff. Dropping it here
|
||||
// lets client-side reads still observe EOF if the server exits before
|
||||
// replying.
|
||||
drop(server_stream_guard);
|
||||
let message = client
|
||||
.receive::<EscalateResponse>()
|
||||
.await
|
||||
@@ -147,12 +128,6 @@ mod tests {
|
||||
use super::*;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio::time::sleep;
|
||||
use tokio::time::timeout;
|
||||
|
||||
#[test]
|
||||
fn duplicate_fd_for_transfer_does_not_close_original() {
|
||||
@@ -166,83 +141,4 @@ mod tests {
|
||||
|
||||
assert_ne!(unsafe { libc::fcntl(original_fd, libc::F_GETFD) }, -1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn connect_escalation_stream_keeps_sender_alive_until_first_request_write()
|
||||
-> anyhow::Result<()> {
|
||||
let (server_datagram, client_datagram) = AsyncDatagramSocket::pair()?;
|
||||
let client_task = tokio::spawn(async move {
|
||||
let (client_stream, server_stream_guard) =
|
||||
connect_escalation_stream(client_datagram).await?;
|
||||
let guard_fd = server_stream_guard.as_raw_fd();
|
||||
assert_ne!(unsafe { libc::fcntl(guard_fd, libc::F_GETFD) }, -1);
|
||||
client_stream
|
||||
.send(EscalateRequest {
|
||||
file: PathBuf::from("/bin/echo"),
|
||||
argv: vec!["echo".to_string(), "hello".to_string()],
|
||||
workdir: AbsolutePathBuf::current_dir()?,
|
||||
env: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
drop(server_stream_guard);
|
||||
assert_eq!(-1, unsafe { libc::fcntl(guard_fd, libc::F_GETFD) });
|
||||
let response = client_stream.receive::<EscalateResponse>().await?;
|
||||
Ok::<EscalateResponse, anyhow::Error>(response)
|
||||
});
|
||||
|
||||
let (_, mut fds) = server_datagram.receive_with_fds().await?;
|
||||
assert_eq!(fds.len(), 1);
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
let server_stream = AsyncSocket::from_fd(fds.remove(0))?;
|
||||
let request = server_stream.receive::<EscalateRequest>().await?;
|
||||
assert_eq!(request.file, PathBuf::from("/bin/echo"));
|
||||
assert_eq!(request.argv, vec!["echo".to_string(), "hello".to_string()]);
|
||||
|
||||
let expected = EscalateResponse {
|
||||
action: EscalateAction::Deny {
|
||||
reason: Some("not now".to_string()),
|
||||
},
|
||||
};
|
||||
server_stream.send(expected.clone()).await?;
|
||||
let response = client_task.await??;
|
||||
assert_eq!(response, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dropping_guard_after_request_write_preserves_server_eof() -> anyhow::Result<()> {
|
||||
let (server_datagram, client_datagram) = AsyncDatagramSocket::pair()?;
|
||||
let client_task = tokio::spawn(async move {
|
||||
let (client_stream, server_stream_guard) =
|
||||
connect_escalation_stream(client_datagram).await?;
|
||||
client_stream
|
||||
.send(EscalateRequest {
|
||||
file: PathBuf::from("/bin/echo"),
|
||||
argv: vec!["echo".to_string()],
|
||||
workdir: AbsolutePathBuf::current_dir()?,
|
||||
env: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
drop(server_stream_guard);
|
||||
let err = timeout(
|
||||
Duration::from_millis(250),
|
||||
client_stream.receive::<EscalateResponse>(),
|
||||
)
|
||||
.await
|
||||
.expect("server close should not hang the client")
|
||||
.expect_err("expected EOF after server closes without replying");
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
|
||||
Ok::<(), anyhow::Error>(())
|
||||
});
|
||||
|
||||
let (_, mut fds) = server_datagram.receive_with_fds().await?;
|
||||
assert_eq!(fds.len(), 1);
|
||||
let server_stream = AsyncSocket::from_fd(fds.remove(0))?;
|
||||
let request = server_stream.receive::<EscalateRequest>().await?;
|
||||
assert_eq!(request.file, PathBuf::from("/bin/echo"));
|
||||
drop(server_stream);
|
||||
|
||||
client_task.await??;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ schema and Responses API tool primitives that no longer need to live in
|
||||
- `AdditionalProperties`
|
||||
- `ToolDefinition`
|
||||
- `ToolSpec`
|
||||
- `ConfiguredToolSpec`
|
||||
- `ResponsesApiTool`
|
||||
- `FreeformTool`
|
||||
- `FreeformToolFormat`
|
||||
@@ -23,6 +24,7 @@ schema and Responses API tool primitives that no longer need to live in
|
||||
- `parse_tool_input_schema()`
|
||||
- `parse_dynamic_tool()`
|
||||
- `parse_mcp_tool()`
|
||||
- `create_tools_json_for_responses_api()`
|
||||
- `mcp_call_tool_result_output_schema()`
|
||||
- `tool_definition_to_responses_api_tool()`
|
||||
- `dynamic_tool_to_responses_api_tool()`
|
||||
|
||||
@@ -25,6 +25,8 @@ pub use responses_api::mcp_tool_to_deferred_responses_api_tool;
|
||||
pub use responses_api::mcp_tool_to_responses_api_tool;
|
||||
pub use responses_api::tool_definition_to_responses_api_tool;
|
||||
pub use tool_definition::ToolDefinition;
|
||||
pub use tool_spec::ConfiguredToolSpec;
|
||||
pub use tool_spec::ResponsesApiWebSearchFilters;
|
||||
pub use tool_spec::ResponsesApiWebSearchUserLocation;
|
||||
pub use tool_spec::ToolSpec;
|
||||
pub use tool_spec::create_tools_json_for_responses_api;
|
||||
|
||||
@@ -6,6 +6,7 @@ use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters;
|
||||
use codex_protocol::config_types::WebSearchUserLocation as ConfigWebSearchUserLocation;
|
||||
use codex_protocol::config_types::WebSearchUserLocationType;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
/// When serialized as JSON, this produces a valid "Tool" in the OpenAI
|
||||
/// Responses API.
|
||||
@@ -60,6 +61,41 @@ impl ToolSpec {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ConfiguredToolSpec {
|
||||
pub spec: ToolSpec,
|
||||
pub supports_parallel_tool_calls: bool,
|
||||
}
|
||||
|
||||
impl ConfiguredToolSpec {
|
||||
pub fn new(spec: ToolSpec, supports_parallel_tool_calls: bool) -> Self {
|
||||
Self {
|
||||
spec,
|
||||
supports_parallel_tool_calls,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
self.spec.name()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns JSON values that are compatible with Function Calling in the
|
||||
/// Responses API:
|
||||
/// https://platform.openai.com/docs/guides/function-calling?api-mode=responses
|
||||
pub fn create_tools_json_for_responses_api(
|
||||
tools: &[ToolSpec],
|
||||
) -> Result<Vec<Value>, serde_json::Error> {
|
||||
let mut tools_json = Vec::new();
|
||||
|
||||
for tool in tools {
|
||||
let json = serde_json::to_value(tool)?;
|
||||
tools_json.push(json);
|
||||
}
|
||||
|
||||
Ok(tools_json)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
pub struct ResponsesApiWebSearchFilters {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::ConfiguredToolSpec;
|
||||
use super::ResponsesApiWebSearchFilters;
|
||||
use super::ResponsesApiWebSearchUserLocation;
|
||||
use super::ToolSpec;
|
||||
@@ -6,6 +7,7 @@ use crate::FreeformTool;
|
||||
use crate::FreeformToolFormat;
|
||||
use crate::JsonSchema;
|
||||
use crate::ResponsesApiTool;
|
||||
use crate::create_tools_json_for_responses_api;
|
||||
use codex_protocol::config_types::WebSearchContextSize;
|
||||
use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters;
|
||||
use codex_protocol::config_types::WebSearchUserLocation as ConfigWebSearchUserLocation;
|
||||
@@ -79,6 +81,29 @@ fn tool_spec_name_covers_all_variants() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_tool_spec_name_delegates_to_tool_spec() {
|
||||
assert_eq!(
|
||||
ConfiguredToolSpec::new(
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "lookup_order".to_string(),
|
||||
description: "Look up an order".to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::Object {
|
||||
properties: BTreeMap::new(),
|
||||
required: None,
|
||||
additional_properties: None,
|
||||
},
|
||||
output_schema: None,
|
||||
}),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
)
|
||||
.name(),
|
||||
"lookup_order"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_config_converts_to_responses_api_types() {
|
||||
assert_eq!(
|
||||
@@ -107,6 +132,40 @@ fn web_search_config_converts_to_responses_api_types() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_tools_json_for_responses_api_includes_top_level_name() {
|
||||
assert_eq!(
|
||||
create_tools_json_for_responses_api(&[ToolSpec::Function(ResponsesApiTool {
|
||||
name: "demo".to_string(),
|
||||
description: "A demo tool".to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::Object {
|
||||
properties: BTreeMap::from([(
|
||||
"foo".to_string(),
|
||||
JsonSchema::String { description: None },
|
||||
)]),
|
||||
required: None,
|
||||
additional_properties: None,
|
||||
},
|
||||
output_schema: None,
|
||||
})])
|
||||
.expect("serialize tools"),
|
||||
vec![json!({
|
||||
"type": "function",
|
||||
"name": "demo",
|
||||
"description": "A demo tool",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"foo": { "type": "string" }
|
||||
},
|
||||
},
|
||||
})]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_tool_spec_serializes_expected_wire_shape() {
|
||||
assert_eq!(
|
||||
|
||||
12
justfile
12
justfile
@@ -67,17 +67,21 @@ bazel-lock-check:
|
||||
./scripts/check-module-bazel-lock.sh
|
||||
|
||||
bazel-test:
|
||||
bazel test --test_tag_filters=-argument-comment-lint //... --keep_going
|
||||
bazel test --test_tag_filters=-argument-comment-lint --keep_going -- //... -//third_party/v8:all
|
||||
|
||||
bazel-clippy:
|
||||
bazel build --config=clippy -- //codex-rs/... -//codex-rs/v8-poc:all
|
||||
|
||||
[no-cd]
|
||||
bazel-argument-comment-lint:
|
||||
bazel build --config=argument-comment-lint -- //codex-rs/...
|
||||
./.github/scripts/run-bazel-ci.sh -- build --config=argument-comment-lint --keep_going -- //codex-rs/...
|
||||
|
||||
# Fast local iteration helper: prefer the fully remote Linux path, even on
|
||||
# macOS, to keep local CPU/RAM use down. For best same-clone reruns, keep
|
||||
# BuildBuddy auth plus `build/test --watchfs` in `~/.bazelrc`.
|
||||
[no-cd]
|
||||
bazel-remote-test:
|
||||
bazel test --test_tag_filters=-argument-comment-lint //... --config=remote --platforms=//:rbe --keep_going
|
||||
./.github/scripts/run-bazel-ci.sh --print-failed-test-logs --use-node-test-env --ci-config=ci-linux -- test --test_tag_filters=-argument-comment-lint --test_verbose_timeout_warnings -- //... -//third_party/v8:all
|
||||
|
||||
build-for-release:
|
||||
bazel build //codex-rs/cli:release_binaries --config=remote
|
||||
@@ -102,7 +106,7 @@ write-hooks-schema:
|
||||
[no-cd]
|
||||
argument-comment-lint *args:
|
||||
if [ "$#" -eq 0 ]; then \
|
||||
bazel build --config=argument-comment-lint -- //codex-rs/...; \
|
||||
./.github/scripts/run-bazel-ci.sh -- build --config=argument-comment-lint --keep_going -- //codex-rs/...; \
|
||||
else \
|
||||
./tools/argument-comment-lint/run-prebuilt-linter.py "$@"; \
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user