Merge 8600629232 into sapling-pr-archive-bolinfest

This commit is contained in:
Michael Bolin
2026-06-24 18:24:48 -07:00
committed by GitHub
23 changed files with 353 additions and 36 deletions
+8
View File
@@ -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,
+2 -1
View File
@@ -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
"#
),
+9
View File
@@ -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"])
+76
View File
@@ -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<()>>());
+4 -6
View File
@@ -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"
},
+2
View File
@@ -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,
},
),
] {
+7 -1
View File
@@ -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),
}))
}
+23
View File
@@ -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,
+31
View File
@@ -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(&[
+1
View File
@@ -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, &params.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(),
+5 -2
View File
@@ -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");
+8 -4
View File
@@ -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)
+15 -6
View File
@@ -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(
+1
View File
@@ -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()
});
}
+7 -2
View File
@@ -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
+3
View File
@@ -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 {
-8
View File
@@ -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",