mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
Merge 8600629232 into sapling-pr-archive-bolinfest
This commit is contained in:
@@ -15,6 +15,7 @@ use anyhow::anyhow;
|
||||
pub use backend::BackendKind;
|
||||
use backend::BackendPaths;
|
||||
use codex_app_server_protocol::RemoteControlConnectionStatus;
|
||||
use codex_app_server_protocol::RemoteControlPairingStartResponse;
|
||||
use codex_app_server_transport::app_server_control_socket_path;
|
||||
use codex_utils_home_dir::find_codex_home;
|
||||
use managed_install::managed_codex_bin;
|
||||
@@ -225,6 +226,13 @@ pub async fn enable_remote_control_on_socket(
|
||||
.await
|
||||
}
|
||||
|
||||
/// Starts a manual pairing session through an already-running daemon app-server.
|
||||
pub async fn start_remote_control_pairing() -> Result<RemoteControlPairingStartResponse> {
|
||||
ensure_supported_platform()?;
|
||||
let daemon = Daemon::from_environment()?;
|
||||
remote_control_client::start_pairing(&daemon.socket_path).await
|
||||
}
|
||||
|
||||
pub async fn set_remote_control(mode: RemoteControlMode) -> Result<RemoteControlOutput> {
|
||||
ensure_supported_platform()?;
|
||||
Daemon::from_environment()?.set_remote_control(mode).await
|
||||
|
||||
@@ -12,6 +12,8 @@ use codex_app_server_protocol::RemoteControlDisableParams;
|
||||
use codex_app_server_protocol::RemoteControlDisableResponse;
|
||||
use codex_app_server_protocol::RemoteControlEnableParams;
|
||||
use codex_app_server_protocol::RemoteControlEnableResponse;
|
||||
use codex_app_server_protocol::RemoteControlPairingStartParams;
|
||||
use codex_app_server_protocol::RemoteControlPairingStartResponse;
|
||||
use codex_app_server_protocol::RemoteControlStatusChangedNotification;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use serde::de::DeserializeOwned;
|
||||
@@ -53,6 +55,35 @@ pub(crate) async fn disable_remote_control(socket_path: &Path) -> Result<RemoteC
|
||||
Ok(RemoteControlReadyStatus::from(response))
|
||||
}
|
||||
|
||||
pub(crate) async fn start_pairing(socket_path: &Path) -> Result<RemoteControlPairingStartResponse> {
|
||||
let mut websocket = client::connect(socket_path).await?;
|
||||
initialize_client(&mut websocket).await?;
|
||||
let params = serde_json::to_value(RemoteControlPairingStartParams { manual_code: true })?;
|
||||
send_remote_control_request(
|
||||
&mut websocket,
|
||||
REMOTE_CONTROL_REQUEST_ID.clone(),
|
||||
"remoteControl/pairing/start",
|
||||
Some(params),
|
||||
)
|
||||
.await?;
|
||||
let response = match read_remote_control_response(
|
||||
&mut websocket,
|
||||
&REMOTE_CONTROL_REQUEST_ID,
|
||||
"remoteControl/pairing/start",
|
||||
)
|
||||
.await?
|
||||
{
|
||||
RemoteControlRpcResponse::Success(response) => response,
|
||||
RemoteControlRpcResponse::InvalidParams => {
|
||||
return Err(anyhow!(
|
||||
"remoteControl/pairing/start rejected manual pairing parameters"
|
||||
));
|
||||
}
|
||||
};
|
||||
websocket.close(None).await.ok();
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub(crate) async fn enable_remote_control_with_connect_retry(
|
||||
socket_path: &Path,
|
||||
connect_timeout: Duration,
|
||||
@@ -538,6 +569,53 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_pairing_requests_manual_code() -> Result<()> {
|
||||
let dir = TempDir::new()?;
|
||||
let socket_path = dir.path().join("app-server.sock");
|
||||
let listener = UnixListener::bind(&socket_path).await?;
|
||||
let server_task = tokio::spawn(async move {
|
||||
let mut websocket = accept_initialized_client(listener).await?;
|
||||
let pairing = client::read_message(&mut websocket).await?;
|
||||
let JSONRPCMessage::Request(pairing) = pairing else {
|
||||
panic!("expected remoteControl/pairing/start request");
|
||||
};
|
||||
assert_eq!(pairing.id, REMOTE_CONTROL_REQUEST_ID);
|
||||
assert_eq!(pairing.method, "remoteControl/pairing/start");
|
||||
assert_eq!(
|
||||
pairing.params,
|
||||
Some(serde_json::json!({ "manualCode": true }))
|
||||
);
|
||||
client::send_message(
|
||||
&mut websocket,
|
||||
&JSONRPCMessage::Response(JSONRPCResponse {
|
||||
id: REMOTE_CONTROL_REQUEST_ID,
|
||||
result: serde_json::to_value(RemoteControlPairingStartResponse {
|
||||
pairing_code: "pairing-code".to_string(),
|
||||
manual_pairing_code: Some("ABCD-EFGH".to_string()),
|
||||
environment_id: "env_test".to_string(),
|
||||
expires_at: 1_700_000_000,
|
||||
})?,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
Ok::<_, anyhow::Error>(())
|
||||
});
|
||||
|
||||
let response = start_pairing(&socket_path).await?;
|
||||
server_task.await??;
|
||||
assert_eq!(
|
||||
response,
|
||||
RemoteControlPairingStartResponse {
|
||||
pairing_code: "pairing-code".to_string(),
|
||||
manual_pairing_code: Some("ABCD-EFGH".to_string()),
|
||||
environment_id: "env_test".to_string(),
|
||||
expires_at: 1_700_000_000,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct EnableScenario {
|
||||
initial_notification: Option<RemoteControlStatusChangedNotification>,
|
||||
enable_response: RemoteControlStatusChangedNotification,
|
||||
|
||||
@@ -167,7 +167,8 @@ wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
|
||||
[features]
|
||||
[features.current_time_reminder]
|
||||
enabled = true
|
||||
sleep_tool = true
|
||||
"#
|
||||
),
|
||||
|
||||
@@ -3559,6 +3559,15 @@ mod tests {
|
||||
assert!(err.to_string().contains("remote-control"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_control_pair_parses() {
|
||||
let cli = MultitoolCli::try_parse_from(["codex", "remote-control", "pair"]).expect("parse");
|
||||
let Some(Subcommand::RemoteControl(remote_control)) = &cli.subcommand else {
|
||||
panic!("expected remote-control subcommand");
|
||||
};
|
||||
assert_eq!(remote_control.subcommand_name(), "remote-control pair");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_flag_parses_for_interactive_root() {
|
||||
let cli = MultitoolCli::try_parse_from(["codex", "--remote", "unix://codex.sock"])
|
||||
|
||||
@@ -13,6 +13,7 @@ use codex_app_server_daemon::RemoteControlReadyOutput as AppServerRemoteControlR
|
||||
use codex_app_server_daemon::RemoteControlReadyStatus as AppServerRemoteControlReadyStatus;
|
||||
use codex_app_server_daemon::RemoteControlStartOutput as AppServerRemoteControlStartOutput;
|
||||
use codex_app_server_protocol::RemoteControlConnectionStatus;
|
||||
use codex_app_server_protocol::RemoteControlPairingStartResponse;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
@@ -43,6 +44,7 @@ impl RemoteControlCommand {
|
||||
None => "remote-control",
|
||||
Some(RemoteControlSubcommand::Start) => "remote-control start",
|
||||
Some(RemoteControlSubcommand::Stop) => "remote-control stop",
|
||||
Some(RemoteControlSubcommand::Pair) => "remote-control pair",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +56,9 @@ enum RemoteControlSubcommand {
|
||||
|
||||
/// Stop the app-server daemon.
|
||||
Stop,
|
||||
|
||||
/// Create and print a short-lived manual pairing code.
|
||||
Pair,
|
||||
}
|
||||
|
||||
pub(crate) async fn run(
|
||||
@@ -82,6 +87,10 @@ pub(crate) async fn run(
|
||||
let output = codex_app_server_daemon::run(AppServerLifecycleCommand::Stop).await?;
|
||||
print_remote_control_stop_output(&output, command.json)?;
|
||||
}
|
||||
Some(RemoteControlSubcommand::Pair) => {
|
||||
let output = codex_app_server_daemon::start_remote_control_pairing().await?;
|
||||
print_remote_control_pairing_output(&output, command.json)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -451,6 +460,29 @@ fn print_remote_control_stop_output(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_remote_control_pairing_output(
|
||||
output: &RemoteControlPairingStartResponse,
|
||||
json: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
println!("{}", format_remote_control_pairing_output(output, json)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_remote_control_pairing_output(
|
||||
output: &RemoteControlPairingStartResponse,
|
||||
json: bool,
|
||||
) -> anyhow::Result<String> {
|
||||
if json {
|
||||
return Ok(serde_json::to_string(output)?);
|
||||
}
|
||||
|
||||
let manual_pairing_code = output
|
||||
.manual_pairing_code
|
||||
.as_deref()
|
||||
.context("remote-control pairing response did not include a manual pairing code")?;
|
||||
Ok(format!("Pairing code: {manual_pairing_code}"))
|
||||
}
|
||||
|
||||
fn remote_control_stop_human_message(output: &AppServerLifecycleOutput) -> String {
|
||||
match output.status {
|
||||
AppServerLifecycleStatus::Stopped => "Remote control stopped.".to_string(),
|
||||
@@ -509,6 +541,15 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn pairing_response(manual_pairing_code: Option<&str>) -> RemoteControlPairingStartResponse {
|
||||
RemoteControlPairingStartResponse {
|
||||
pairing_code: "pairing-code".to_string(),
|
||||
manual_pairing_code: manual_pairing_code.map(str::to_string),
|
||||
environment_id: "env_test".to_string(),
|
||||
expires_at: 1_700_000_000,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_control_human_start_messages_use_server_name() {
|
||||
assert_eq!(
|
||||
@@ -629,6 +670,41 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_control_pairing_human_output_labels_the_manual_code() {
|
||||
assert_eq!(
|
||||
format_remote_control_pairing_output(&pairing_response(Some("ABCD-EFGH")), false)
|
||||
.expect("manual pairing output"),
|
||||
"Pairing code: ABCD-EFGH"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_control_pairing_json_output_preserves_pairing_artifacts() {
|
||||
let output =
|
||||
format_remote_control_pairing_output(&pairing_response(Some("ABCD-EFGH")), true)
|
||||
.expect("pairing JSON output");
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&output).expect("valid JSON"),
|
||||
json!({
|
||||
"pairingCode": "pairing-code",
|
||||
"manualPairingCode": "ABCD-EFGH",
|
||||
"environmentId": "env_test",
|
||||
"expiresAt": 1_700_000_000,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_control_pairing_human_output_requires_manual_code() {
|
||||
assert_eq!(
|
||||
format_remote_control_pairing_output(&pairing_response(None), false)
|
||||
.expect_err("missing manual pairing code should fail")
|
||||
.to_string(),
|
||||
"remote-control pairing response did not include a manual pairing code"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn foreground_wait_aborts_app_server_on_stop_signal() {
|
||||
let app_server_task = tokio::spawn(std::future::pending::<std::io::Result<()>>());
|
||||
|
||||
@@ -643,9 +643,6 @@
|
||||
"skill_mcp_dependency_install": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sleep_tool": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sqlite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -820,6 +817,10 @@
|
||||
"format": "uint64",
|
||||
"minimum": 1.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"sleep_tool": {
|
||||
"description": "Expose the input-interruptible `clock.sleep` tool.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -4956,9 +4957,6 @@
|
||||
"skill_mcp_dependency_install": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sleep_tool": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sqlite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -625,10 +625,12 @@ current_time_reminder = true
|
||||
enabled = true
|
||||
reminder_interval_seconds = 4
|
||||
clock_source = "external"
|
||||
sleep_tool = true
|
||||
"#,
|
||||
CurrentTimeReminderConfig {
|
||||
reminder_interval_seconds: 4,
|
||||
clock_source: CurrentTimeSource::External,
|
||||
sleep_tool: true,
|
||||
},
|
||||
),
|
||||
] {
|
||||
|
||||
@@ -1037,7 +1037,7 @@ pub struct Config {
|
||||
pub token_budget: Option<TokenBudgetConfig>,
|
||||
/// Shared token budget for the root thread and its sub-agents.
|
||||
pub rollout_budget: Option<RolloutBudgetConfig>,
|
||||
/// Current-time reminder configuration, when enabled.
|
||||
/// Current-time reminder and clock tool configuration, when enabled.
|
||||
pub current_time_reminder: Option<CurrentTimeReminderConfig>,
|
||||
|
||||
/// Centralized feature flags; source of truth for feature gating.
|
||||
@@ -1120,6 +1120,8 @@ pub struct RolloutBudgetConfig {
|
||||
pub struct CurrentTimeReminderConfig {
|
||||
pub reminder_interval_seconds: u64,
|
||||
pub clock_source: CurrentTimeSource,
|
||||
/// Whether to expose the input-interruptible `clock.sleep` tool.
|
||||
pub sleep_tool: bool,
|
||||
}
|
||||
|
||||
impl Default for CurrentTimeReminderConfig {
|
||||
@@ -1127,6 +1129,7 @@ impl Default for CurrentTimeReminderConfig {
|
||||
Self {
|
||||
reminder_interval_seconds: 1,
|
||||
clock_source: CurrentTimeSource::System,
|
||||
sleep_tool: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2694,6 +2697,9 @@ fn resolve_current_time_reminder_config(
|
||||
clock_source: base
|
||||
.and_then(|config| config.clock_source)
|
||||
.unwrap_or(default.clock_source),
|
||||
sleep_tool: base
|
||||
.and_then(|config| config.sleep_tool)
|
||||
.unwrap_or(default.sleep_tool),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,16 @@ use codex_protocol::ThreadId;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::config_types::EnvironmentVariablePattern;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::shell_environment;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use codex_protocol::shell_environment::CODEX_THREAD_ID_ENV_VAR;
|
||||
|
||||
/// Informational name of the active permission profile. Child processes can
|
||||
/// overwrite this value, so it must not be treated as proof of enforcement.
|
||||
pub const CODEX_PERMISSION_PROFILE_ENV_VAR: &str = "CODEX_PERMISSION_PROFILE";
|
||||
|
||||
/// Construct an environment map based on the rules in the specified policy. The
|
||||
/// resulting map can be passed directly to `Command::envs()` after calling
|
||||
/// `env_clear()` to ensure no unintended variables are leaked to the spawned
|
||||
@@ -25,6 +30,24 @@ pub fn create_env(
|
||||
shell_environment::create_env(policy, thread_id.as_deref())
|
||||
}
|
||||
|
||||
/// Injects the selected named permission profile into a shell tool's environment.
|
||||
///
|
||||
/// This is applied after the shell environment policy so the runtime-selected
|
||||
/// profile wins over inherited or configured values.
|
||||
pub(crate) fn inject_permission_profile_env(
|
||||
env: &mut HashMap<String, String>,
|
||||
active_permission_profile: Option<&ActivePermissionProfile>,
|
||||
) {
|
||||
if let Some(active_permission_profile) = active_permission_profile {
|
||||
env.insert(
|
||||
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
|
||||
active_permission_profile.id.clone(),
|
||||
);
|
||||
} else {
|
||||
env.remove(CODEX_PERMISSION_PROFILE_ENV_VAR);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_os = "windows"))]
|
||||
fn create_env_from_vars<I>(
|
||||
vars: I,
|
||||
|
||||
@@ -10,6 +10,37 @@ fn make_vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_permission_profile_env_overrides_policy_value() {
|
||||
let mut env = HashMap::from([(
|
||||
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
|
||||
"stale-profile".to_string(),
|
||||
)]);
|
||||
|
||||
inject_permission_profile_env(
|
||||
&mut env,
|
||||
Some(&ActivePermissionProfile::new("current-profile")),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
env.get(CODEX_PERMISSION_PROFILE_ENV_VAR)
|
||||
.map(String::as_str),
|
||||
Some("current-profile")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_permission_profile_env_removes_stale_value_without_active_profile() {
|
||||
let mut env = HashMap::from([(
|
||||
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
|
||||
"stale-profile".to_string(),
|
||||
)]);
|
||||
|
||||
inject_permission_profile_env(&mut env, None);
|
||||
|
||||
assert_eq!(env.get(CODEX_PERMISSION_PROFILE_ENV_VAR), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_core_inherit_defaults_keep_sensitive_vars() {
|
||||
let vars = make_vars(&[
|
||||
|
||||
@@ -361,6 +361,7 @@ mod tests {
|
||||
enabled: Some(true),
|
||||
reminder_interval_seconds: Some(1),
|
||||
clock_source: Some(codex_features::CurrentTimeSource::System),
|
||||
sleep_tool: Some(false),
|
||||
}))
|
||||
);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::exec::ExecParams;
|
||||
use crate::exec_env::create_env;
|
||||
use crate::exec_env::inject_permission_profile_env;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::maybe_emit_implicit_skill_invocation;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
@@ -99,15 +100,19 @@ impl ShellCommandHandler {
|
||||
let use_login_shell = Self::resolve_use_login_shell(params.login, allow_login_shell)?;
|
||||
let command = Self::base_command(shell, ¶ms.command, use_login_shell);
|
||||
|
||||
let mut env = create_env(
|
||||
&turn_context.config.permissions.shell_environment_policy,
|
||||
Some(session.thread_id),
|
||||
);
|
||||
let active_permission_profile = turn_context.config.permissions.active_permission_profile();
|
||||
inject_permission_profile_env(&mut env, active_permission_profile.as_ref());
|
||||
|
||||
Ok(ExecParams {
|
||||
command,
|
||||
cwd,
|
||||
expiration: params.timeout_ms.into(),
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env: create_env(
|
||||
&turn_context.config.permissions.shell_environment_policy,
|
||||
Some(session.thread_id),
|
||||
),
|
||||
env,
|
||||
network: turn_context.network.clone(),
|
||||
network_environment_id: Some(turn_environment.environment_id.clone()),
|
||||
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::ShellCommandToolCallParams;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::config::PermissionProfileSnapshot;
|
||||
use crate::exec_env::CODEX_PERMISSION_PROFILE_ENV_VAR;
|
||||
use crate::exec_env::create_env;
|
||||
use crate::exec_env::inject_permission_profile_env;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::session::step_context::StepContext;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
@@ -71,7 +75,15 @@ fn assert_safe(shell: &Shell, command: &str) {
|
||||
|
||||
#[tokio::test]
|
||||
async fn shell_command_handler_to_exec_params_uses_selected_environment() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let (session, mut turn_context) = make_session_and_context().await;
|
||||
let permission_profile = turn_context.config.permissions.permission_profile().clone();
|
||||
Arc::make_mut(&mut turn_context.config)
|
||||
.permissions
|
||||
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(
|
||||
permission_profile,
|
||||
ActivePermissionProfile::new("test-profile"),
|
||||
))
|
||||
.expect("set active permission profile");
|
||||
|
||||
let command = "echo hello".to_string();
|
||||
let workdir = Some("subdir".to_string());
|
||||
@@ -99,10 +111,12 @@ async fn shell_command_handler_to_exec_params_uses_selected_environment() {
|
||||
PathUri::from_abs_path(&selected_cwd),
|
||||
Some(selected_shell),
|
||||
);
|
||||
let expected_env = create_env(
|
||||
let mut expected_env = create_env(
|
||||
&turn_context.config.permissions.shell_environment_policy,
|
||||
Some(session.thread_id),
|
||||
);
|
||||
let active_permission_profile = turn_context.config.permissions.active_permission_profile();
|
||||
inject_permission_profile_env(&mut expected_env, active_permission_profile.as_ref());
|
||||
|
||||
let params = ShellCommandToolCallParams {
|
||||
command,
|
||||
@@ -129,6 +143,12 @@ async fn shell_command_handler_to_exec_params_uses_selected_environment() {
|
||||
assert_eq!(exec_params.command, expected_command);
|
||||
assert_eq!(exec_params.cwd, expected_cwd);
|
||||
assert_eq!(exec_params.env, expected_env);
|
||||
assert_eq!(
|
||||
exec_params.env.get(CODEX_PERMISSION_PROFILE_ENV_VAR),
|
||||
active_permission_profile
|
||||
.as_ref()
|
||||
.map(|profile| &profile.id)
|
||||
);
|
||||
assert_eq!(exec_params.network, turn_context.network);
|
||||
assert_eq!(
|
||||
exec_params.network_environment_id.as_deref(),
|
||||
|
||||
@@ -4,6 +4,7 @@ Module: runtimes
|
||||
Concrete ToolRuntime implementations for specific tools. Each runtime stays
|
||||
small and focused and reuses the orchestrator for approvals + sandbox + retry.
|
||||
*/
|
||||
use crate::exec_env::CODEX_PERMISSION_PROFILE_ENV_VAR;
|
||||
use crate::exec_env::CODEX_THREAD_ID_ENV_VAR;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::shell::Shell;
|
||||
@@ -287,8 +288,10 @@ pub(crate) fn maybe_wrap_shell_lc_with_snapshot(
|
||||
.map(|arg| format!(" '{}'", shell_single_quote(arg)))
|
||||
.collect::<String>();
|
||||
let mut override_env = explicit_env_overrides.clone();
|
||||
if let Some(thread_id) = env.get(CODEX_THREAD_ID_ENV_VAR) {
|
||||
override_env.insert(CODEX_THREAD_ID_ENV_VAR.to_string(), thread_id.clone());
|
||||
for key in [CODEX_THREAD_ID_ENV_VAR, CODEX_PERMISSION_PROFILE_ENV_VAR] {
|
||||
if let Some(value) = env.get(key) {
|
||||
override_env.insert(key.to_string(), value.clone());
|
||||
}
|
||||
}
|
||||
let (override_captures, override_exports) = build_override_exports(&override_env);
|
||||
let (proxy_captures, proxy_exports) = build_proxy_env_exports();
|
||||
|
||||
@@ -534,6 +534,44 @@ fn maybe_wrap_shell_lc_with_snapshot_restores_codex_thread_id_from_env() {
|
||||
assert_eq!(String::from_utf8_lossy(&output.stdout), "nested-thread");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_wrap_shell_lc_with_snapshot_restores_permission_profile_from_env() {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
let snapshot_path = dir.path().join("snapshot.sh");
|
||||
std::fs::write(
|
||||
&snapshot_path,
|
||||
"# Snapshot file\nexport CODEX_PERMISSION_PROFILE='parent-profile'\n",
|
||||
)
|
||||
.expect("write snapshot");
|
||||
let (session_shell, shell_snapshot) =
|
||||
shell_with_snapshot(ShellType::Bash, "/bin/bash", snapshot_path.abs());
|
||||
let command = vec![
|
||||
"/bin/bash".to_string(),
|
||||
"-lc".to_string(),
|
||||
"printenv CODEX_PERMISSION_PROFILE".to_string(),
|
||||
];
|
||||
let env = HashMap::from([(
|
||||
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
|
||||
"current-profile".to_string(),
|
||||
)]);
|
||||
let rewritten = maybe_wrap_shell_lc_with_snapshot(
|
||||
&command,
|
||||
&session_shell,
|
||||
Some(&shell_snapshot),
|
||||
&HashMap::new(),
|
||||
&env,
|
||||
&RuntimePathPrepends::default(),
|
||||
);
|
||||
let output = Command::new(&rewritten[0])
|
||||
.args(&rewritten[1..])
|
||||
.env(CODEX_PERMISSION_PROFILE_ENV_VAR, "current-profile")
|
||||
.output()
|
||||
.expect("run rewritten command");
|
||||
|
||||
assert!(output.status.success(), "command failed: {output:?}");
|
||||
assert_eq!(String::from_utf8_lossy(&output.stdout), "current-profile\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_wrap_shell_lc_with_snapshot_restores_proxy_env_from_process_env() {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
|
||||
@@ -744,10 +744,14 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut
|
||||
|
||||
if features.enabled(Feature::CurrentTimeReminder) {
|
||||
planned_tools.add(CurrentTimeHandler);
|
||||
}
|
||||
|
||||
if features.enabled(Feature::SleepTool) {
|
||||
planned_tools.add(SleepHandler);
|
||||
if turn_context
|
||||
.config
|
||||
.current_time_reminder
|
||||
.as_ref()
|
||||
.is_some_and(|config| config.sleep_tool)
|
||||
{
|
||||
planned_tools.add(SleepHandler);
|
||||
}
|
||||
}
|
||||
|
||||
if tool_suggest_enabled(turn_context)
|
||||
|
||||
@@ -31,6 +31,7 @@ use codex_tools::ToolSpec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::config::CurrentTimeReminderConfig;
|
||||
use crate::session::step_context::StepContext;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
@@ -721,19 +722,27 @@ async fn host_context_gates_agent_job_tools() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sleep_tool_follows_feature_gate() {
|
||||
async fn sleep_tool_follows_current_time_config() {
|
||||
let disabled = probe(|turn| {
|
||||
set_feature(turn, Feature::SleepTool, /*enabled*/ false);
|
||||
set_feature(turn, Feature::CurrentTimeReminder, /*enabled*/ true);
|
||||
})
|
||||
.await;
|
||||
disabled.assert_visible_lacks(&["clock"]);
|
||||
assert_eq!(disabled.namespace_function_names("clock"), ["curr_time"]);
|
||||
|
||||
let enabled = probe(|turn| {
|
||||
set_feature(turn, Feature::SleepTool, /*enabled*/ true);
|
||||
set_feature(turn, Feature::CurrentTimeReminder, /*enabled*/ true);
|
||||
let mut config = (*turn.config).clone();
|
||||
config.current_time_reminder = Some(CurrentTimeReminderConfig {
|
||||
sleep_tool: true,
|
||||
..CurrentTimeReminderConfig::default()
|
||||
});
|
||||
turn.config = Arc::new(config);
|
||||
})
|
||||
.await;
|
||||
enabled.assert_visible_contains(&["clock"]);
|
||||
assert_eq!(enabled.namespace_function_names("clock"), ["sleep"]);
|
||||
assert_eq!(
|
||||
enabled.namespace_function_names("clock"),
|
||||
["curr_time", "sleep"]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -15,6 +15,7 @@ use uuid::Uuid;
|
||||
use crate::codex_thread::BackgroundTerminalInfo;
|
||||
use crate::exec_env::CODEX_THREAD_ID_ENV_VAR;
|
||||
use crate::exec_env::create_env;
|
||||
use crate::exec_env::inject_permission_profile_env;
|
||||
use crate::exec_policy::ExecApprovalRequest;
|
||||
use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::ExecRequest;
|
||||
@@ -1110,6 +1111,8 @@ impl UnifiedExecProcessManager {
|
||||
CODEX_THREAD_ID_ENV_VAR.to_string(),
|
||||
context.session.thread_id.to_string(),
|
||||
);
|
||||
let active_permission_profile = context.turn.config.permissions.active_permission_profile();
|
||||
inject_permission_profile_env(&mut env, active_permission_profile.as_ref());
|
||||
let env = apply_unified_exec_env(env);
|
||||
let exec_server_env_config = ExecServerEnvConfig {
|
||||
policy: exec_env_policy_from_shell_policy(
|
||||
|
||||
@@ -883,6 +883,7 @@ text(JSON.stringify(result));
|
||||
config.current_time_reminder = Some(CurrentTimeReminderConfig {
|
||||
reminder_interval_seconds: 3_000,
|
||||
clock_source: CurrentTimeSource::System,
|
||||
..CurrentTimeReminderConfig::default()
|
||||
});
|
||||
},
|
||||
)
|
||||
|
||||
@@ -86,6 +86,7 @@ fn enable_current_time_reminder(
|
||||
config.current_time_reminder = Some(CurrentTimeReminderConfig {
|
||||
reminder_interval_seconds: interval,
|
||||
clock_source,
|
||||
..CurrentTimeReminderConfig::default()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use core_test_support::test_codex::local_selections;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::config::CurrentTimeReminderConfig;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::items::SleepItem;
|
||||
@@ -390,8 +391,12 @@ async fn any_new_input_interrupts_sleep() {
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::SleepTool)
|
||||
.expect("test config should allow feature update");
|
||||
.enable(Feature::CurrentTimeReminder)
|
||||
.expect("test config should allow current-time reminders");
|
||||
config.current_time_reminder = Some(CurrentTimeReminderConfig {
|
||||
sleep_tool: true,
|
||||
..CurrentTimeReminderConfig::default()
|
||||
});
|
||||
})
|
||||
.build_with_streaming_server(&server)
|
||||
.await
|
||||
|
||||
@@ -151,6 +151,9 @@ pub struct CurrentTimeReminderConfigToml {
|
||||
pub reminder_interval_seconds: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub clock_source: Option<CurrentTimeSource>,
|
||||
/// Expose the input-interruptible `clock.sleep` tool.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sleep_tool: Option<bool>,
|
||||
}
|
||||
|
||||
impl FeatureConfig for CurrentTimeReminderConfigToml {
|
||||
|
||||
@@ -219,8 +219,6 @@ pub enum Feature {
|
||||
RolloutBudget,
|
||||
/// Add current-time reminders to model-visible context.
|
||||
CurrentTimeReminder,
|
||||
/// Expose an input-interruptible sleep tool.
|
||||
SleepTool,
|
||||
/// Route MCP tool approval prompts through the MCP elicitation request path.
|
||||
ToolCallMcpElicitation,
|
||||
/// Prompt Codex Apps connector auth failures through MCP URL elicitations.
|
||||
@@ -1243,12 +1241,6 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::SleepTool,
|
||||
key: "sleep_tool",
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::CollaborationModes,
|
||||
key: "collaboration_modes",
|
||||
|
||||
Reference in New Issue
Block a user