mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
Merge branch 'main' into etraut/fix-agent-picker-deadlock
This commit is contained in:
@@ -70,13 +70,9 @@ pub use crate::remote::RemoteAppServerEndpoint;
|
||||
/// module exists so clients can remove a direct `codex-core` dependency
|
||||
/// while legacy startup/config paths are migrated to RPCs.
|
||||
pub mod legacy_core {
|
||||
pub use codex_core::DEFAULT_AGENTS_MD_FILENAME;
|
||||
pub use codex_core::LOCAL_AGENTS_MD_FILENAME;
|
||||
pub use codex_core::McpManager;
|
||||
pub use codex_core::check_execpolicy_for_warnings;
|
||||
pub use codex_core::format_exec_policy_error_with_source;
|
||||
pub use codex_core::grant_read_root_non_elevated;
|
||||
pub use codex_core::web_search_detail;
|
||||
|
||||
pub mod config {
|
||||
pub use codex_core::config::*;
|
||||
@@ -86,10 +82,6 @@ pub mod legacy_core {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod connectors {
|
||||
pub use codex_core::connectors::*;
|
||||
}
|
||||
|
||||
pub mod otel_init {
|
||||
pub use codex_core::otel_init::*;
|
||||
}
|
||||
@@ -98,22 +90,10 @@ pub mod legacy_core {
|
||||
pub use codex_core::personality_migration::*;
|
||||
}
|
||||
|
||||
pub mod review_format {
|
||||
pub use codex_core::review_format::*;
|
||||
}
|
||||
|
||||
pub mod review_prompts {
|
||||
pub use codex_core::review_prompts::*;
|
||||
}
|
||||
|
||||
pub mod test_support {
|
||||
pub use codex_core::test_support::*;
|
||||
}
|
||||
|
||||
pub mod util {
|
||||
pub use codex_core::util::*;
|
||||
}
|
||||
|
||||
pub mod windows_sandbox {
|
||||
pub use codex_core::windows_sandbox::*;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ pub fn create_create_goal_tool() -> ToolSpec {
|
||||
(
|
||||
"objective".to_string(),
|
||||
JsonSchema::string(Some(
|
||||
"Required. The concrete objective to start pursuing. This starts a new active goal only when no goal is currently defined; if a goal already exists, this tool fails."
|
||||
"Required. The concrete objective to start pursuing. This starts a new active goal when no goal exists or replaces the current goal when it is complete."
|
||||
.to_string(),
|
||||
)),
|
||||
),
|
||||
@@ -44,7 +44,7 @@ pub fn create_create_goal_tool() -> ToolSpec {
|
||||
name: CREATE_GOAL_TOOL_NAME.to_string(),
|
||||
description: format!(
|
||||
r#"Create a goal only when explicitly requested by the user or system/developer instructions; do not infer goals from ordinary tasks.
|
||||
Set token_budget only when an explicit token budget is requested. Fails if a goal exists; use {UPDATE_GOAL_TOOL_NAME} only for status."#
|
||||
Set token_budget only when an explicit token budget is requested. Fails if an unfinished goal exists; use {UPDATE_GOAL_TOOL_NAME} only for status."#
|
||||
),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
|
||||
@@ -191,7 +191,7 @@ impl GoalToolExecutor {
|
||||
.map_err(|err| FunctionCallError::RespondToModel(format!("failed to create goal: {err}")))?
|
||||
.ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"cannot create a new goal because this thread already has a goal; use update_goal only when the existing goal is complete"
|
||||
"cannot create a new goal because this thread has an unfinished goal; complete the existing goal first"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -124,7 +124,7 @@ async fn goal_tools_hidden_for_review_subagents() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn installed_goal_tools_reject_duplicate_goal_creation() -> anyhow::Result<()> {
|
||||
async fn installed_goal_tools_only_replace_complete_goal() -> anyhow::Result<()> {
|
||||
let runtime = test_runtime().await?;
|
||||
let thread_id = test_thread_id()?;
|
||||
seed_thread_metadata(runtime.as_ref(), thread_id).await?;
|
||||
@@ -152,10 +152,31 @@ async fn installed_goal_tools_reject_duplicate_goal_creation() -> anyhow::Result
|
||||
assert_eq!(
|
||||
err,
|
||||
FunctionCallError::RespondToModel(
|
||||
"cannot create a new goal because this thread already has a goal; use update_goal only when the existing goal is complete"
|
||||
"cannot create a new goal because this thread has an unfinished goal; complete the existing goal first"
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
|
||||
let update_tool = tool_by_name(&tools, "update_goal");
|
||||
update_tool
|
||||
.handle(tool_call(
|
||||
"update_goal",
|
||||
"call-complete-goal",
|
||||
json!({ "status": "complete" }),
|
||||
))
|
||||
.await?;
|
||||
|
||||
let invocation = tool_call(
|
||||
"create_goal",
|
||||
"call-create-goal-3",
|
||||
json!({ "objective": "replacement goal" }),
|
||||
);
|
||||
let output = create_tool.handle(invocation.clone()).await?;
|
||||
let result = output.code_mode_result(&invocation.payload);
|
||||
|
||||
assert_eq!(json!("replacement goal"), result["goal"]["objective"]);
|
||||
assert_eq!(json!("active"), result["goal"]["status"]);
|
||||
assert_eq!(json!(0), result["goal"]["tokensUsed"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -107,9 +107,9 @@ When a request is blocked, the proxy responds with `403` and includes:
|
||||
- `blocked-by-method-policy`
|
||||
- `blocked-by-policy`
|
||||
|
||||
In "limited" mode, only `GET`, `HEAD`, and `OPTIONS` are allowed. HTTPS `CONNECT` requests require
|
||||
MITM to enforce limited-mode method policy; otherwise they are blocked. SOCKS5 remains blocked in
|
||||
limited mode.
|
||||
In "limited" mode, only `GET`, `HEAD`, and `OPTIONS` are allowed. HTTPS `CONNECT` requests and
|
||||
HTTPS SOCKS5 TCP targets on `:443` require MITM to enforce limited-mode method policy; otherwise
|
||||
they are blocked. SOCKS5 UDP and non-HTTPS SOCKS5 TCP remain blocked in limited mode.
|
||||
|
||||
Websocket clients typically tunnel `wss://` through HTTPS `CONNECT`; those CONNECT targets still go
|
||||
through the same host allowlist/denylist checks.
|
||||
@@ -215,7 +215,8 @@ what it can reasonably guarantee.
|
||||
allowlisted (best-effort DNS lookup).
|
||||
- Limited mode enforcement:
|
||||
- only `GET`, `HEAD`, and `OPTIONS` are allowed
|
||||
- HTTPS `CONNECT` remains a tunnel; limited-mode method enforcement does not apply to HTTPS
|
||||
- HTTPS `CONNECT` requests and HTTPS SOCKS5 TCP targets on `:443` require MITM so the proxy can
|
||||
enforce limited-mode method policy; SOCKS5 UDP and non-HTTPS SOCKS5 TCP remain blocked
|
||||
- Listener safety defaults:
|
||||
- the HTTP proxy listener clamps non-loopback binds unless explicitly enabled via
|
||||
`dangerously_allow_non_loopback_proxy`
|
||||
|
||||
@@ -276,7 +276,7 @@ impl NetworkProxySettings {
|
||||
pub enum NetworkMode {
|
||||
/// Limited (read-only) access: only GET/HEAD/OPTIONS are allowed for HTTP. HTTPS CONNECT is
|
||||
/// blocked unless MITM is enabled so the proxy can enforce method policy on inner requests.
|
||||
/// SOCKS5 remains blocked in limited mode.
|
||||
/// SOCKS5 UDP and non-HTTPS SOCKS5 TCP remain blocked in limited mode.
|
||||
Limited,
|
||||
/// Full network access: all HTTP methods are allowed. HTTPS CONNECTs are tunneled directly.
|
||||
/// MITM hooks do not currently make full mode enter MITM.
|
||||
|
||||
@@ -21,10 +21,12 @@ use rama_core::Layer;
|
||||
use rama_core::Service;
|
||||
use rama_core::bytes::Bytes;
|
||||
use rama_core::error::BoxError;
|
||||
use rama_core::extensions::ExtensionsMut;
|
||||
use rama_core::extensions::ExtensionsRef;
|
||||
use rama_core::futures::stream::Stream;
|
||||
use rama_core::futures::stream::Stream as FuturesStream;
|
||||
use rama_core::rt::Executor;
|
||||
use rama_core::service::service_fn;
|
||||
use rama_core::stream::Stream;
|
||||
use rama_http::Body;
|
||||
use rama_http::BodyDataStream;
|
||||
use rama_http::HeaderMap;
|
||||
@@ -135,17 +137,25 @@ impl MitmState {
|
||||
|
||||
/// Terminate the upgraded CONNECT stream with a generated leaf cert and proxy inner HTTPS traffic.
|
||||
pub(crate) async fn mitm_tunnel(upgraded: Upgraded) -> Result<()> {
|
||||
let mitm = upgraded
|
||||
mitm_stream(upgraded).await
|
||||
}
|
||||
|
||||
/// Terminate a raw client stream with a generated leaf cert and proxy inner HTTPS traffic.
|
||||
pub(crate) async fn mitm_stream<S>(stream: S) -> Result<()>
|
||||
where
|
||||
S: Stream + Unpin + ExtensionsMut,
|
||||
{
|
||||
let mitm = stream
|
||||
.extensions()
|
||||
.get::<Arc<MitmState>>()
|
||||
.cloned()
|
||||
.context("missing MITM state")?;
|
||||
let app_state = upgraded
|
||||
let app_state = stream
|
||||
.extensions()
|
||||
.get::<Arc<NetworkProxyState>>()
|
||||
.cloned()
|
||||
.context("missing app state")?;
|
||||
let target = upgraded
|
||||
let target = stream
|
||||
.extensions()
|
||||
.get::<ProxyTarget>()
|
||||
.context("missing proxy target")?
|
||||
@@ -154,7 +164,7 @@ pub(crate) async fn mitm_tunnel(upgraded: Upgraded) -> Result<()> {
|
||||
let target_host = normalize_host(&target.host.to_string());
|
||||
let target_port = target.port;
|
||||
let acceptor_data = mitm.tls_acceptor_data_for_host(&target_host)?;
|
||||
let mode = upgraded
|
||||
let mode = stream
|
||||
.extensions()
|
||||
.get::<NetworkMode>()
|
||||
.copied()
|
||||
@@ -169,7 +179,7 @@ pub(crate) async fn mitm_tunnel(upgraded: Upgraded) -> Result<()> {
|
||||
mitm,
|
||||
});
|
||||
|
||||
let executor = upgraded
|
||||
let executor = stream
|
||||
.extensions()
|
||||
.get::<Executor>()
|
||||
.cloned()
|
||||
@@ -194,7 +204,7 @@ pub(crate) async fn mitm_tunnel(upgraded: Upgraded) -> Result<()> {
|
||||
.into_layer(http_service);
|
||||
|
||||
https_service
|
||||
.serve(upgraded)
|
||||
.serve(stream)
|
||||
.await
|
||||
.map_err(|err| anyhow!("MITM serve error: {err}"))?;
|
||||
Ok(())
|
||||
@@ -456,7 +466,7 @@ struct InspectStream<T> {
|
||||
max_body_bytes: usize,
|
||||
}
|
||||
|
||||
impl<T: BodyLoggable> Stream for InspectStream<T> {
|
||||
impl<T: BodyLoggable> FuturesStream for InspectStream<T> {
|
||||
type Item = Result<Bytes, BoxError>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::config::NetworkMode;
|
||||
use crate::connect_policy::TargetCheckedTcpConnector;
|
||||
use crate::mitm;
|
||||
use crate::network_policy::BlockDecisionAuditEventArgs;
|
||||
use crate::network_policy::NetworkDecision;
|
||||
use crate::network_policy::NetworkDecisionSource;
|
||||
@@ -24,10 +25,17 @@ use anyhow::Result;
|
||||
use rama_core::Layer;
|
||||
use rama_core::Service;
|
||||
use rama_core::error::BoxError;
|
||||
use rama_core::extensions::Extensions;
|
||||
use rama_core::extensions::ExtensionsMut;
|
||||
use rama_core::extensions::ExtensionsRef;
|
||||
use rama_core::layer::AddInputExtensionLayer;
|
||||
use rama_core::service::service_fn;
|
||||
use rama_net::address::HostWithPort;
|
||||
use rama_net::client::EstablishedClientConnection;
|
||||
use rama_net::proxy::ProxyRequest;
|
||||
use rama_net::proxy::ProxyTarget;
|
||||
use rama_net::proxy::StreamForwardService;
|
||||
use rama_net::stream::Socket;
|
||||
use rama_net::stream::SocketInfo;
|
||||
use rama_socks5::Socks5Acceptor;
|
||||
use rama_socks5::server::DefaultConnector;
|
||||
@@ -40,8 +48,14 @@ use rama_tcp::server::TcpListener;
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::net::TcpListener as StdTcpListener;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::Context as TaskContext;
|
||||
use std::task::Poll;
|
||||
use std::time::Instant;
|
||||
use tokio::io::AsyncRead;
|
||||
use tokio::io::AsyncWrite;
|
||||
use tokio::io::ReadBuf;
|
||||
use tracing::error;
|
||||
use tracing::info;
|
||||
use tracing::warn;
|
||||
@@ -88,7 +102,9 @@ async fn run_socks5_with_listener(
|
||||
|
||||
match state.network_mode().await {
|
||||
Ok(NetworkMode::Limited) => {
|
||||
info!("SOCKS5 is blocked in limited mode; set mode=\"full\" to allow SOCKS5");
|
||||
info!(
|
||||
"SOCKS5 UDP and non-HTTPS SOCKS5 TCP are blocked in limited mode; HTTPS SOCKS5 TCP requires MITM inspection"
|
||||
);
|
||||
}
|
||||
Ok(NetworkMode::Full) => {}
|
||||
Err(err) => {
|
||||
@@ -106,7 +122,10 @@ async fn run_socks5_with_listener(
|
||||
}
|
||||
});
|
||||
|
||||
let socks_connector = DefaultConnector::default().with_connector(policy_tcp_connector);
|
||||
let socks_proxy = service_fn(|request| async move { proxy_socks5_tcp(request).await });
|
||||
let socks_connector = DefaultConnector::default()
|
||||
.with_connector(policy_tcp_connector)
|
||||
.with_service(socks_proxy);
|
||||
let base = Socks5Acceptor::new().with_connector(socks_connector);
|
||||
|
||||
if enable_socks5_udp {
|
||||
@@ -135,7 +154,7 @@ async fn handle_socks5_tcp(
|
||||
req: TcpRequest,
|
||||
tcp_connector: TargetCheckedTcpConnector,
|
||||
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
||||
) -> Result<EstablishedClientConnection<TcpStream, TcpRequest>, BoxError> {
|
||||
) -> Result<EstablishedClientConnection<Socks5TcpConnection, TcpRequest>, BoxError> {
|
||||
let app_state = req
|
||||
.extensions()
|
||||
.get::<Arc<NetworkProxyState>>()
|
||||
@@ -144,6 +163,7 @@ async fn handle_socks5_tcp(
|
||||
|
||||
let host = normalize_host(&req.authority.host.to_string());
|
||||
let port = req.authority.port;
|
||||
let target = req.authority.clone();
|
||||
if host.is_empty() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid host").into());
|
||||
}
|
||||
@@ -196,94 +216,52 @@ async fn handle_socks5_tcp(
|
||||
}
|
||||
}
|
||||
|
||||
match app_state.network_mode().await {
|
||||
Ok(NetworkMode::Limited) => {
|
||||
emit_socks_block_decision_audit_event(
|
||||
&app_state,
|
||||
NetworkDecisionSource::ModeGuard,
|
||||
REASON_METHOD_NOT_ALLOWED,
|
||||
NetworkProtocol::Socks5Tcp,
|
||||
host.as_str(),
|
||||
port,
|
||||
client.as_deref(),
|
||||
);
|
||||
let details = PolicyDecisionDetails {
|
||||
decision: NetworkPolicyDecision::Deny,
|
||||
reason: REASON_METHOD_NOT_ALLOWED,
|
||||
source: NetworkDecisionSource::ModeGuard,
|
||||
protocol: NetworkProtocol::Socks5Tcp,
|
||||
host: &host,
|
||||
port,
|
||||
};
|
||||
let _ = app_state
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: REASON_METHOD_NOT_ALLOWED.to_string(),
|
||||
client: client.clone(),
|
||||
method: None,
|
||||
mode: Some(NetworkMode::Limited),
|
||||
protocol: "socks5".to_string(),
|
||||
decision: Some(details.decision.as_str().to_string()),
|
||||
source: Some(details.source.as_str().to_string()),
|
||||
port: Some(port),
|
||||
}))
|
||||
.await;
|
||||
let client = client.as_deref().unwrap_or_default();
|
||||
warn!(
|
||||
"SOCKS blocked by method policy (client={client}, host={host}, mode=limited, allowed_methods=GET, HEAD, OPTIONS)"
|
||||
);
|
||||
return Err(policy_denied_error(REASON_METHOD_NOT_ALLOWED, &details).into());
|
||||
}
|
||||
Ok(NetworkMode::Full) => {}
|
||||
let mode = match app_state.network_mode().await {
|
||||
Ok(mode) => mode,
|
||||
Err(err) => {
|
||||
error!("failed to evaluate method policy: {err}");
|
||||
return Err(io::Error::other("proxy error").into());
|
||||
}
|
||||
}
|
||||
|
||||
match app_state.host_has_mitm_hooks(&host).await {
|
||||
Ok(true) => {
|
||||
emit_socks_block_decision_audit_event(
|
||||
&app_state,
|
||||
NetworkDecisionSource::ModeGuard,
|
||||
REASON_MITM_REQUIRED,
|
||||
NetworkProtocol::Socks5Tcp,
|
||||
host.as_str(),
|
||||
port,
|
||||
client.as_deref(),
|
||||
);
|
||||
let details = PolicyDecisionDetails {
|
||||
decision: NetworkPolicyDecision::Deny,
|
||||
reason: REASON_MITM_REQUIRED,
|
||||
source: NetworkDecisionSource::ModeGuard,
|
||||
protocol: NetworkProtocol::Socks5Tcp,
|
||||
host: &host,
|
||||
port,
|
||||
};
|
||||
let _ = app_state
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: REASON_MITM_REQUIRED.to_string(),
|
||||
client: client.clone(),
|
||||
method: None,
|
||||
mode: Some(NetworkMode::Full),
|
||||
protocol: "socks5".to_string(),
|
||||
decision: Some(details.decision.as_str().to_string()),
|
||||
source: Some(details.source.as_str().to_string()),
|
||||
port: Some(port),
|
||||
}))
|
||||
.await;
|
||||
let client = client.as_deref().unwrap_or_default();
|
||||
warn!(
|
||||
"SOCKS blocked; MITM required to enforce HTTPS policy (client={client}, host={host}, mode=full)"
|
||||
);
|
||||
return Err(policy_denied_error(REASON_MITM_REQUIRED, &details).into());
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(err) => {
|
||||
error!("failed to inspect MITM hooks for {host}: {err}");
|
||||
return Err(io::Error::other("proxy error").into());
|
||||
}
|
||||
};
|
||||
// SOCKS5 only exposes host and port, so only the default HTTPS port is identifiable as a
|
||||
// TLS stream that the HTTPS MITM path can safely terminate.
|
||||
let socks5_tcp_target_is_https = port == 443;
|
||||
if mode == NetworkMode::Limited && !socks5_tcp_target_is_https {
|
||||
emit_socks_block_decision_audit_event(
|
||||
&app_state,
|
||||
NetworkDecisionSource::ModeGuard,
|
||||
REASON_METHOD_NOT_ALLOWED,
|
||||
NetworkProtocol::Socks5Tcp,
|
||||
host.as_str(),
|
||||
port,
|
||||
client.as_deref(),
|
||||
);
|
||||
let details = PolicyDecisionDetails {
|
||||
decision: NetworkPolicyDecision::Deny,
|
||||
reason: REASON_METHOD_NOT_ALLOWED,
|
||||
source: NetworkDecisionSource::ModeGuard,
|
||||
protocol: NetworkProtocol::Socks5Tcp,
|
||||
host: &host,
|
||||
port,
|
||||
};
|
||||
let _ = app_state
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: REASON_METHOD_NOT_ALLOWED.to_string(),
|
||||
client: client.clone(),
|
||||
method: None,
|
||||
mode: Some(NetworkMode::Limited),
|
||||
protocol: "socks5".to_string(),
|
||||
decision: Some(details.decision.as_str().to_string()),
|
||||
source: Some(details.source.as_str().to_string()),
|
||||
port: Some(port),
|
||||
}))
|
||||
.await;
|
||||
let client = client.as_deref().unwrap_or_default();
|
||||
warn!(
|
||||
"SOCKS blocked; limited mode only supports HTTPS MITM (client={client}, host={host}, port={port})"
|
||||
);
|
||||
return Err(policy_denied_error(REASON_METHOD_NOT_ALLOWED, &details).into());
|
||||
}
|
||||
|
||||
let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs {
|
||||
@@ -337,9 +315,85 @@ async fn handle_socks5_tcp(
|
||||
}
|
||||
}
|
||||
|
||||
let host_has_mitm_hooks = match app_state.host_has_mitm_hooks(&host).await {
|
||||
Ok(has_hooks) => has_hooks,
|
||||
Err(err) => {
|
||||
error!("failed to inspect MITM hooks for {host}: {err}");
|
||||
return Err(io::Error::other("proxy error").into());
|
||||
}
|
||||
};
|
||||
let mitm_state = match app_state.mitm_state().await {
|
||||
Ok(state) => state,
|
||||
Err(err) => {
|
||||
error!("failed to load MITM state: {err}");
|
||||
return Err(io::Error::other("proxy error").into());
|
||||
}
|
||||
};
|
||||
let socks_needs_mitm =
|
||||
socks5_tcp_target_is_https && (mode == NetworkMode::Limited || host_has_mitm_hooks);
|
||||
if (host_has_mitm_hooks && !socks5_tcp_target_is_https)
|
||||
|| (socks_needs_mitm && mitm_state.is_none())
|
||||
{
|
||||
emit_socks_block_decision_audit_event(
|
||||
&app_state,
|
||||
NetworkDecisionSource::ModeGuard,
|
||||
REASON_MITM_REQUIRED,
|
||||
NetworkProtocol::Socks5Tcp,
|
||||
host.as_str(),
|
||||
port,
|
||||
client.as_deref(),
|
||||
);
|
||||
let details = PolicyDecisionDetails {
|
||||
decision: NetworkPolicyDecision::Deny,
|
||||
reason: REASON_MITM_REQUIRED,
|
||||
source: NetworkDecisionSource::ModeGuard,
|
||||
protocol: NetworkProtocol::Socks5Tcp,
|
||||
host: &host,
|
||||
port,
|
||||
};
|
||||
let _ = app_state
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: REASON_MITM_REQUIRED.to_string(),
|
||||
client: client.clone(),
|
||||
method: None,
|
||||
mode: Some(mode),
|
||||
protocol: "socks5".to_string(),
|
||||
decision: Some(details.decision.as_str().to_string()),
|
||||
source: Some(details.source.as_str().to_string()),
|
||||
port: Some(port),
|
||||
}))
|
||||
.await;
|
||||
let client = client.as_deref().unwrap_or_default();
|
||||
warn!(
|
||||
"SOCKS blocked; MITM required to enforce HTTPS policy (client={client}, host={host}, mode={mode:?}, hooked_host={host_has_mitm_hooks}, https_target={socks5_tcp_target_is_https})"
|
||||
);
|
||||
return Err(policy_denied_error(REASON_MITM_REQUIRED, &details).into());
|
||||
}
|
||||
|
||||
if socks_needs_mitm && let Some(mitm_state) = mitm_state {
|
||||
let client = client.as_deref().unwrap_or_default();
|
||||
info!("SOCKS MITM enabled (client={client}, host={host}, port={port}, mode={mode:?})");
|
||||
return Ok(EstablishedClientConnection {
|
||||
input: req,
|
||||
conn: Socks5TcpConnection::Mitm {
|
||||
target,
|
||||
mode,
|
||||
mitm: mitm_state,
|
||||
extensions: Extensions::new(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
info!("SOCKS upstream dial started (host={host}, port={port})");
|
||||
let connect_started_at = Instant::now();
|
||||
let result = tcp_connector.serve(req).await;
|
||||
let result = tcp_connector.serve(req).await.map(|connection| {
|
||||
let EstablishedClientConnection { input, conn } = connection;
|
||||
EstablishedClientConnection {
|
||||
input,
|
||||
conn: Socks5TcpConnection::Direct(conn),
|
||||
}
|
||||
});
|
||||
match &result {
|
||||
Ok(_) => info!(
|
||||
"SOCKS upstream dial established (host={host}, port={port}, elapsed_ms={})",
|
||||
@@ -353,6 +407,113 @@ async fn handle_socks5_tcp(
|
||||
result
|
||||
}
|
||||
|
||||
/// Internal connector output for SOCKS5 TCP. MITM requests do not dial upstream before the
|
||||
/// inner HTTPS request is inspected, so they carry the target metadata instead of a socket.
|
||||
#[derive(Debug)]
|
||||
enum Socks5TcpConnection {
|
||||
Direct(TcpStream),
|
||||
Mitm {
|
||||
target: HostWithPort,
|
||||
mode: NetworkMode,
|
||||
mitm: Arc<mitm::MitmState>,
|
||||
extensions: Extensions,
|
||||
},
|
||||
}
|
||||
|
||||
impl AsyncRead for Socks5TcpConnection {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut TaskContext<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
match self.get_mut() {
|
||||
Self::Direct(stream) => Pin::new(stream).poll_read(cx, buf),
|
||||
Self::Mitm { .. } => Poll::Ready(Ok(())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for Socks5TcpConnection {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut TaskContext<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
match self.get_mut() {
|
||||
Self::Direct(stream) => Pin::new(stream).poll_write(cx, buf),
|
||||
Self::Mitm { .. } => Poll::Ready(Ok(buf.len())),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<io::Result<()>> {
|
||||
match self.get_mut() {
|
||||
Self::Direct(stream) => Pin::new(stream).poll_flush(cx),
|
||||
Self::Mitm { .. } => Poll::Ready(Ok(())),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<io::Result<()>> {
|
||||
match self.get_mut() {
|
||||
Self::Direct(stream) => Pin::new(stream).poll_shutdown(cx),
|
||||
Self::Mitm { .. } => Poll::Ready(Ok(())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Socket for Socks5TcpConnection {
|
||||
fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
match self {
|
||||
Self::Direct(stream) => stream.local_addr(),
|
||||
Self::Mitm { .. } => Ok(SocketAddr::from(([0, 0, 0, 0], 0))),
|
||||
}
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
match self {
|
||||
Self::Direct(stream) => stream.peer_addr(),
|
||||
Self::Mitm { .. } => Ok(SocketAddr::from(([0, 0, 0, 0], 0))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionsRef for Socks5TcpConnection {
|
||||
fn extensions(&self) -> &Extensions {
|
||||
match self {
|
||||
Self::Direct(stream) => stream.extensions(),
|
||||
Self::Mitm { extensions, .. } => extensions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionsMut for Socks5TcpConnection {
|
||||
fn extensions_mut(&mut self) -> &mut Extensions {
|
||||
match self {
|
||||
Self::Direct(stream) => stream.extensions_mut(),
|
||||
Self::Mitm { extensions, .. } => extensions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn proxy_socks5_tcp(
|
||||
request: ProxyRequest<TcpStream, Socks5TcpConnection>,
|
||||
) -> Result<(), BoxError> {
|
||||
let ProxyRequest { mut source, target } = request;
|
||||
match target {
|
||||
Socks5TcpConnection::Direct(target) => StreamForwardService::default()
|
||||
.serve(ProxyRequest { source, target })
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
Socks5TcpConnection::Mitm {
|
||||
target, mode, mitm, ..
|
||||
} => {
|
||||
source.extensions_mut().insert(ProxyTarget(target));
|
||||
source.extensions_mut().insert(mode);
|
||||
source.extensions_mut().insert(mitm);
|
||||
mitm::mitm_stream(source).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn inspect_socks5_udp(
|
||||
request: RelayRequest,
|
||||
state: Arc<NetworkProxyState>,
|
||||
@@ -554,7 +715,6 @@ mod tests {
|
||||
use crate::network_policy::test_support::find_event_by_name;
|
||||
use crate::runtime::ConfigReloader;
|
||||
use crate::runtime::ConfigState;
|
||||
use crate::runtime::network_proxy_state_for_policy;
|
||||
use crate::state::NetworkProxyConstraints;
|
||||
use crate::state::build_config_state;
|
||||
use async_trait::async_trait;
|
||||
@@ -567,6 +727,11 @@ mod tests {
|
||||
use std::net::IpAddr;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
// Managed MITM CA files live under the shared test CODEX_HOME, so MITM-enabled config state
|
||||
// must be materialized one test at a time.
|
||||
static MITM_CONFIG_STATE_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[derive(Clone)]
|
||||
struct StaticReloader {
|
||||
@@ -590,6 +755,10 @@ mod tests {
|
||||
|
||||
fn state_for_settings(network: NetworkProxySettings) -> Arc<NetworkProxyState> {
|
||||
let config = NetworkProxyConfig { network };
|
||||
let _mitm_config_state_guard = config
|
||||
.network
|
||||
.mitm
|
||||
.then(|| MITM_CONFIG_STATE_LOCK.lock().unwrap());
|
||||
let state = build_config_state(config, NetworkProxyConstraints::default()).unwrap();
|
||||
let reloader = Arc::new(StaticReloader {
|
||||
state: state.clone(),
|
||||
@@ -639,43 +808,56 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn handle_socks5_tcp_blocks_hooked_host_in_full_mode() {
|
||||
let state = Arc::new(network_proxy_state_for_policy(NetworkProxySettings {
|
||||
async fn handle_socks5_tcp_uses_mitm_in_limited_mode() {
|
||||
let mut settings = NetworkProxySettings {
|
||||
enabled: true,
|
||||
mode: NetworkMode::Full,
|
||||
mode: NetworkMode::Limited,
|
||||
mitm: true,
|
||||
mitm_hooks: vec![MitmHookConfig {
|
||||
host: "api.github.com".to_string(),
|
||||
matcher: MitmHookMatchConfig {
|
||||
methods: vec!["GET".to_string()],
|
||||
path_prefixes: vec!["/".to_string()],
|
||||
..MitmHookMatchConfig::default()
|
||||
},
|
||||
..MitmHookConfig::default()
|
||||
}],
|
||||
..NetworkProxySettings::default()
|
||||
}));
|
||||
};
|
||||
settings.set_allowed_domains(vec!["example.com".to_string()]);
|
||||
let state = state_for_settings(settings);
|
||||
let mut request =
|
||||
TcpRequest::new(HostWithPort::try_from("api.github.com:443").expect("valid authority"));
|
||||
TcpRequest::new(HostWithPort::try_from("example.com:443").expect("valid authority"));
|
||||
request.extensions_mut().insert(state.clone());
|
||||
|
||||
let result = handle_socks5_tcp(
|
||||
request,
|
||||
TargetCheckedTcpConnector::new(state),
|
||||
/*policy_decider*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("limited-mode HTTPS should use MITM");
|
||||
|
||||
assert!(matches!(result.conn, Socks5TcpConnection::Mitm { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn handle_socks5_tcp_blocks_non_https_in_limited_mode() {
|
||||
let mut settings = NetworkProxySettings {
|
||||
enabled: true,
|
||||
mode: NetworkMode::Limited,
|
||||
..NetworkProxySettings::default()
|
||||
};
|
||||
settings.set_allowed_domains(vec!["example.com".to_string()]);
|
||||
let state = state_for_settings(settings);
|
||||
let mut request =
|
||||
TcpRequest::new(HostWithPort::try_from("example.com:80").expect("valid authority"));
|
||||
request.extensions_mut().insert(state.clone());
|
||||
|
||||
let (result, events) = capture_events(|| async {
|
||||
handle_socks5_tcp(
|
||||
request,
|
||||
TargetCheckedTcpConnector::new(state.clone()),
|
||||
TargetCheckedTcpConnector::new(state),
|
||||
/*policy_decider*/ None,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
assert!(result.is_err(), "hooked host should require MITM");
|
||||
|
||||
let blocked = state.drain_blocked().await.unwrap();
|
||||
assert_eq!(blocked.len(), 1);
|
||||
assert_eq!(blocked[0].reason, REASON_MITM_REQUIRED);
|
||||
assert_eq!(blocked[0].host, "api.github.com");
|
||||
assert_eq!(blocked[0].port, Some(443));
|
||||
assert_eq!(blocked[0].protocol, "socks5");
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"limited-mode non-HTTPS SOCKS should be denied"
|
||||
);
|
||||
|
||||
let event = find_event_by_name(&events, POLICY_DECISION_EVENT_NAME)
|
||||
.expect("expected policy decision event");
|
||||
@@ -684,18 +866,116 @@ mod tests {
|
||||
assert_eq!(event.field("network.policy.source"), Some("mode_guard"));
|
||||
assert_eq!(
|
||||
event.field("network.policy.reason"),
|
||||
Some(REASON_MITM_REQUIRED)
|
||||
Some(REASON_METHOD_NOT_ALLOWED)
|
||||
);
|
||||
assert_eq!(
|
||||
event.field("network.transport.protocol"),
|
||||
Some("socks5_tcp")
|
||||
);
|
||||
assert_eq!(event.field("server.address"), Some("api.github.com"));
|
||||
assert_eq!(event.field("server.port"), Some("443"));
|
||||
assert_eq!(event.field("server.address"), Some("example.com"));
|
||||
assert_eq!(event.field("server.port"), Some("80"));
|
||||
assert_eq!(event.field("http.request.method"), Some("none"));
|
||||
assert_eq!(event.field("client.address"), Some("unknown"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn handle_socks5_tcp_blocks_limited_mode_without_mitm_state() {
|
||||
let mut settings = NetworkProxySettings {
|
||||
enabled: true,
|
||||
mode: NetworkMode::Limited,
|
||||
..NetworkProxySettings::default()
|
||||
};
|
||||
settings.set_allowed_domains(vec!["example.com".to_string()]);
|
||||
let state = state_for_settings(settings);
|
||||
let mut request =
|
||||
TcpRequest::new(HostWithPort::try_from("example.com:443").expect("valid authority"));
|
||||
request.extensions_mut().insert(state.clone());
|
||||
|
||||
let err = handle_socks5_tcp(
|
||||
request,
|
||||
TargetCheckedTcpConnector::new(state),
|
||||
/*policy_decider*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("limited-mode HTTPS requires MITM");
|
||||
|
||||
assert!(
|
||||
format!("{err:?}").contains("MITM required"),
|
||||
"unexpected error: {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn handle_socks5_tcp_uses_mitm_for_hooked_host_in_full_mode() {
|
||||
let mut settings = NetworkProxySettings {
|
||||
enabled: true,
|
||||
mode: NetworkMode::Full,
|
||||
mitm: true,
|
||||
mitm_hooks: vec![MitmHookConfig {
|
||||
host: "api.github.com".to_string(),
|
||||
matcher: MitmHookMatchConfig {
|
||||
methods: vec!["POST".to_string()],
|
||||
path_prefixes: vec!["/repos/openai/".to_string()],
|
||||
..MitmHookMatchConfig::default()
|
||||
},
|
||||
..MitmHookConfig::default()
|
||||
}],
|
||||
..NetworkProxySettings::default()
|
||||
};
|
||||
settings.set_allowed_domains(vec!["api.github.com".to_string()]);
|
||||
let state = state_for_settings(settings);
|
||||
let mut request =
|
||||
TcpRequest::new(HostWithPort::try_from("api.github.com:443").expect("valid authority"));
|
||||
request.extensions_mut().insert(state.clone());
|
||||
|
||||
let result = handle_socks5_tcp(
|
||||
request,
|
||||
TargetCheckedTcpConnector::new(state),
|
||||
/*policy_decider*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("hooked HTTPS should use MITM");
|
||||
|
||||
assert!(matches!(result.conn, Socks5TcpConnection::Mitm { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn handle_socks5_tcp_blocks_hooked_non_https_host_in_full_mode() {
|
||||
let mut settings = NetworkProxySettings {
|
||||
enabled: true,
|
||||
mode: NetworkMode::Full,
|
||||
mitm: true,
|
||||
mitm_hooks: vec![MitmHookConfig {
|
||||
host: "api.github.com".to_string(),
|
||||
matcher: MitmHookMatchConfig {
|
||||
methods: vec!["POST".to_string()],
|
||||
path_prefixes: vec!["/repos/openai/".to_string()],
|
||||
..MitmHookMatchConfig::default()
|
||||
},
|
||||
..MitmHookConfig::default()
|
||||
}],
|
||||
..NetworkProxySettings::default()
|
||||
};
|
||||
settings.set_allowed_domains(vec!["api.github.com".to_string()]);
|
||||
let state = state_for_settings(settings);
|
||||
let mut request =
|
||||
TcpRequest::new(HostWithPort::try_from("api.github.com:80").expect("valid authority"));
|
||||
request.extensions_mut().insert(state.clone());
|
||||
|
||||
let err = handle_socks5_tcp(
|
||||
request,
|
||||
TargetCheckedTcpConnector::new(state),
|
||||
/*policy_decider*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("hooked non-HTTPS SOCKS should require MITM");
|
||||
|
||||
assert!(
|
||||
format!("{err:?}").contains("MITM required"),
|
||||
"unexpected error: {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn inspect_socks5_udp_emits_block_decision_for_mode_guard_deny() {
|
||||
let state = state_for_settings(NetworkProxySettings {
|
||||
|
||||
@@ -141,7 +141,16 @@ INSERT INTO thread_goals (
|
||||
created_at_ms,
|
||||
updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?)
|
||||
ON CONFLICT(thread_id) DO NOTHING
|
||||
ON CONFLICT(thread_id) DO UPDATE SET
|
||||
goal_id = excluded.goal_id,
|
||||
objective = excluded.objective,
|
||||
status = excluded.status,
|
||||
token_budget = excluded.token_budget,
|
||||
tokens_used = 0,
|
||||
time_used_seconds = 0,
|
||||
created_at_ms = excluded.created_at_ms,
|
||||
updated_at_ms = excluded.updated_at_ms
|
||||
WHERE thread_goals.status = 'complete'
|
||||
RETURNING
|
||||
thread_id,
|
||||
goal_id,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Generate a file named AGENTS.md that serves as a contributor guide for this repository.
|
||||
Before writing, check whether AGENTS.md already exists in the current working directory. If it does, do not overwrite or modify it.
|
||||
Your goal is to produce a clear, concise, and well-structured document with descriptive headings and actionable explanations for each section.
|
||||
Follow the outline below, but adapt as needed — add sections if relevant, and omit those that do not apply to this project.
|
||||
|
||||
|
||||
@@ -58,7 +58,6 @@ use crate::bottom_pane::TerminalTitleItem;
|
||||
use crate::bottom_pane::TerminalTitleSetupView;
|
||||
use crate::diff_model::FileChange;
|
||||
use crate::git_action_directives::parse_assistant_markdown;
|
||||
use crate::legacy_core::DEFAULT_AGENTS_MD_FILENAME;
|
||||
use crate::legacy_core::config::Config;
|
||||
use crate::legacy_core::config::Constrained;
|
||||
use crate::legacy_core::config::ConstraintResult;
|
||||
@@ -257,6 +256,11 @@ fn queued_message_edit_hint_binding(
|
||||
.or_else(|| bindings.first().copied())
|
||||
}
|
||||
|
||||
fn normalize_thread_name(name: &str) -> Option<String> {
|
||||
let trimmed = name.trim();
|
||||
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
||||
}
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event::ExitMode;
|
||||
use crate::app_event::PermissionProfileSelection;
|
||||
|
||||
@@ -302,7 +302,7 @@ impl ChatWidget {
|
||||
/*initial_text*/ existing_name.unwrap_or_default().to_string(),
|
||||
/*context_label*/ None,
|
||||
Box::new(move |name: String| {
|
||||
let Some(name) = crate::legacy_core::util::normalize_thread_name(&name) else {
|
||||
let Some(name) = normalize_thread_name(&name) else {
|
||||
tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||
history_cell::new_error_event("Thread name cannot be empty.".to_string()),
|
||||
)));
|
||||
|
||||
@@ -213,14 +213,6 @@ impl ChatWidget {
|
||||
.send(AppEvent::OpenDesktopThread { thread_id });
|
||||
}
|
||||
SlashCommand::Init => {
|
||||
let init_target = self.config.cwd.join(DEFAULT_AGENTS_MD_FILENAME);
|
||||
if init_target.exists() {
|
||||
let message = format!(
|
||||
"{DEFAULT_AGENTS_MD_FILENAME} already exists here. Skipping /init to avoid overwriting it."
|
||||
);
|
||||
self.add_info_message(message, /*hint*/ None);
|
||||
return;
|
||||
}
|
||||
const INIT_PROMPT: &str = include_str!("../../prompt_for_init_command.md");
|
||||
self.submit_user_message(INIT_PROMPT.to_string().into());
|
||||
}
|
||||
@@ -662,7 +654,7 @@ impl ChatWidget {
|
||||
}
|
||||
self.session_telemetry
|
||||
.counter("codex.thread.rename", /*inc*/ 1, &[]);
|
||||
let Some(name) = crate::legacy_core::util::normalize_thread_name(&args) else {
|
||||
let Some(name) = normalize_thread_name(&args) else {
|
||||
self.add_error_message("Thread name cannot be empty.".to_string());
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -565,35 +565,14 @@ async fn ctrl_d_with_modal_open_does_not_quit() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_init_skips_when_project_doc_exists() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let tempdir = tempdir().unwrap();
|
||||
let existing_path = tempdir.path().join(DEFAULT_AGENTS_MD_FILENAME);
|
||||
std::fs::write(&existing_path, "existing instructions").unwrap();
|
||||
chat.config.cwd = tempdir.path().to_path_buf().abs();
|
||||
async fn slash_init_does_not_depend_on_loaded_instruction_sources() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.instruction_source_paths = vec![chat.config.cwd.join("project-instructions.md")];
|
||||
|
||||
submit_composer_text(&mut chat, "/init");
|
||||
|
||||
match op_rx.try_recv() {
|
||||
Err(TryRecvError::Empty) => {}
|
||||
other => panic!("expected no Codex op to be sent, got {other:?}"),
|
||||
}
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected one info message");
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains(DEFAULT_AGENTS_MD_FILENAME),
|
||||
"info message should mention the existing file: {rendered:?}"
|
||||
);
|
||||
assert!(
|
||||
rendered.contains("Skipping /init"),
|
||||
"info message should explain why /init was skipped: {rendered:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(existing_path).unwrap(),
|
||||
"existing instructions"
|
||||
);
|
||||
assert_eq!(chat.input_queue.queued_user_messages.len(), 1);
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
assert_eq!(recall_latest_after_clearing(&mut chat), "/init");
|
||||
}
|
||||
|
||||
|
||||
@@ -185,8 +185,6 @@ fn title_case(s: &str) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::legacy_core::DEFAULT_AGENTS_MD_FILENAME;
|
||||
use crate::legacy_core::LOCAL_AGENTS_MD_FILENAME;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -227,7 +225,7 @@ mod tests {
|
||||
async fn compose_agents_summary_includes_global_agents_path() {
|
||||
let codex_home = TempDir::new().expect("temp codex home");
|
||||
let cwd = TempDir::new().expect("temp cwd");
|
||||
let global_agents_path = codex_home.path().join(DEFAULT_AGENTS_MD_FILENAME);
|
||||
let global_agents_path = codex_home.path().join("global.md");
|
||||
let config = test_config(&codex_home, &cwd).await;
|
||||
|
||||
assert_eq!(
|
||||
@@ -240,7 +238,7 @@ mod tests {
|
||||
async fn compose_agents_summary_names_global_agents_override() {
|
||||
let codex_home = TempDir::new().expect("temp codex home");
|
||||
let cwd = TempDir::new().expect("temp cwd");
|
||||
let override_path = codex_home.path().join(LOCAL_AGENTS_MD_FILENAME);
|
||||
let override_path = codex_home.path().join("override.md");
|
||||
let config = test_config(&codex_home, &cwd).await;
|
||||
|
||||
assert_eq!(
|
||||
@@ -253,8 +251,8 @@ mod tests {
|
||||
async fn compose_agents_summary_orders_global_before_project_agents() {
|
||||
let codex_home = TempDir::new().expect("temp codex home");
|
||||
let cwd = TempDir::new().expect("temp cwd");
|
||||
let global_agents_path = codex_home.path().join(DEFAULT_AGENTS_MD_FILENAME);
|
||||
let project_agents_path = cwd.path().join(DEFAULT_AGENTS_MD_FILENAME);
|
||||
let global_agents_path = codex_home.path().join("global.md");
|
||||
let project_agents_path = cwd.path().join("project.md");
|
||||
let config = test_config(&codex_home, &cwd).await;
|
||||
|
||||
let summary = compose_agents_summary(
|
||||
@@ -270,7 +268,7 @@ mod tests {
|
||||
Some(format_directory_display(&global_agents_path, /*max_width*/ None).as_str())
|
||||
);
|
||||
let project_path = paths.next().expect("project agents path");
|
||||
assert!(project_path.ends_with(DEFAULT_AGENTS_MD_FILENAME));
|
||||
assert!(project_path.ends_with("project.md"));
|
||||
assert_eq!(paths.next(), None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user