mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
feat(tui): expose terminal browser tools
This commit is contained in:
@@ -10,6 +10,8 @@ use crate::app_event::ConnectorsSnapshot;
|
||||
use crate::app_info::app_info_from_api;
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::app_server_session::status_account_display_from_auth_mode;
|
||||
use crate::terminal_browser::TERMINAL_BROWSER_NAMESPACE;
|
||||
use crate::terminal_browser::dynamic_tool_response;
|
||||
use codex_app_server_client::AppServerEvent;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
@@ -232,6 +234,53 @@ impl App {
|
||||
return;
|
||||
}
|
||||
|
||||
if let ServerRequest::DynamicToolCall { request_id, params } = &request
|
||||
&& params.namespace.as_deref() == Some(TERMINAL_BROWSER_NAMESPACE)
|
||||
{
|
||||
if !self.terminal_browser_request_matches_active_thread(¶ms.thread_id) {
|
||||
self.app_event_tx
|
||||
.send(AppEvent::TerminalBrowserToolCompleted {
|
||||
request_id: request_id.clone(),
|
||||
response: dynamic_tool_response(Err(anyhow::anyhow!(
|
||||
"terminal browser permission policy only allows the active TUI thread"
|
||||
))),
|
||||
profile_approval: None,
|
||||
});
|
||||
return;
|
||||
}
|
||||
let Some(browser) = self.terminal_browser_for_active_request().await else {
|
||||
self.app_event_tx
|
||||
.send(AppEvent::TerminalBrowserToolCompleted {
|
||||
request_id: request_id.clone(),
|
||||
response: dynamic_tool_response(Err(anyhow::anyhow!(
|
||||
"terminal browser permission policy disables this TUI session"
|
||||
))),
|
||||
profile_approval: None,
|
||||
});
|
||||
return;
|
||||
};
|
||||
let request_id = request_id.clone();
|
||||
let session_key = params.thread_id.clone();
|
||||
let tool = params.tool.clone();
|
||||
let arguments = params.arguments.clone();
|
||||
let profile_approval = (tool == "profile")
|
||||
.then(|| crate::terminal_browser::requested_profile_command(&arguments))
|
||||
.flatten()
|
||||
.and_then(|command| self.terminal_browser_profile_approval(command));
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = browser.execute(&session_key, &tool, arguments).await;
|
||||
let profile_approval = result.is_ok().then_some(profile_approval).flatten();
|
||||
let response = dynamic_tool_response(result);
|
||||
app_event_tx.send(AppEvent::TerminalBrowserToolCompleted {
|
||||
request_id,
|
||||
response,
|
||||
profile_approval,
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(unsupported) = self
|
||||
.pending_app_server_requests
|
||||
.note_server_request(&request)
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::session_state::MessageHistoryMetadata;
|
||||
use crate::session_state::ThreadSessionState;
|
||||
use crate::status::StatusAccountDisplay;
|
||||
use crate::status::plan_type_display_name;
|
||||
use crate::terminal_browser::dynamic_tool_specs;
|
||||
use crate::terminal_visualization_instructions::with_terminal_visualization_instructions;
|
||||
use codex_app_server_client::AppServerClient;
|
||||
use codex_app_server_client::AppServerEvent;
|
||||
@@ -110,6 +111,7 @@ use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnSteerParams;
|
||||
use codex_app_server_protocol::TurnSteerResponse;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_features::Feature;
|
||||
use codex_otel::TelemetryAuthMode;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::approvals::GuardianAssessmentEvent;
|
||||
@@ -1405,6 +1407,12 @@ fn thread_start_params_from_config(
|
||||
remote_cwd_override: Option<&std::path::Path>,
|
||||
session_start_source: Option<ThreadStartSource>,
|
||||
) -> ThreadStartParams {
|
||||
let dynamic_tools = (matches!(thread_params_mode, ThreadParamsMode::Embedded)
|
||||
&& cfg!(any(target_os = "macos", target_os = "linux"))
|
||||
&& config.features.enabled(Feature::TerminalBrowser)
|
||||
&& config.permissions.network_sandbox_policy().is_enabled()
|
||||
&& config.permissions.network.is_none())
|
||||
.then(dynamic_tool_specs);
|
||||
let permissions = permissions_selection_from_config(config, thread_params_mode);
|
||||
let sandbox = permissions
|
||||
.is_none()
|
||||
@@ -1432,6 +1440,7 @@ fn thread_start_params_from_config(
|
||||
developer_instructions: with_terminal_visualization_instructions(
|
||||
config, /*control_instructions*/ None,
|
||||
),
|
||||
dynamic_tools,
|
||||
..ThreadStartParams::default()
|
||||
}
|
||||
}
|
||||
@@ -1803,6 +1812,37 @@ mod tests {
|
||||
.expect("config should build")
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
async fn build_terminal_browser_config(managed_network: bool) -> Config {
|
||||
let codex_home = tempfile::tempdir().expect("terminal-browser config home");
|
||||
let mut cli_overrides = vec![(
|
||||
"features.terminal_browser".to_string(),
|
||||
toml::Value::Boolean(true),
|
||||
)];
|
||||
if managed_network {
|
||||
cli_overrides.push((
|
||||
"features.network_proxy".to_string(),
|
||||
toml::Value::Boolean(true),
|
||||
));
|
||||
}
|
||||
ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.loader_overrides(codex_config::LoaderOverrides::without_managed_config_for_tests())
|
||||
.cli_overrides(cli_overrides)
|
||||
.harness_overrides(ConfigOverrides {
|
||||
permission_profile: Some(PermissionProfile::workspace_write_with(
|
||||
&[],
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
/*exclude_tmpdir_env_var*/ false,
|
||||
/*exclude_slash_tmp*/ false,
|
||||
)),
|
||||
..ConfigOverrides::default()
|
||||
})
|
||||
.build()
|
||||
.await
|
||||
.expect("terminal-browser config should build")
|
||||
}
|
||||
|
||||
fn rate_limit_snapshot(limit_id: &str) -> RateLimitSnapshot {
|
||||
RateLimitSnapshot {
|
||||
limit_id: Some(limit_id.to_string()),
|
||||
@@ -1910,6 +1950,73 @@ mod tests {
|
||||
assert_eq!(params.thread_source, Some(ThreadSource::User));
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
#[tokio::test]
|
||||
async fn terminal_browser_dynamic_tools_require_supported_embedded_network_mode() {
|
||||
let direct_config = build_terminal_browser_config(/*managed_network*/ false).await;
|
||||
let managed_config = build_terminal_browser_config(/*managed_network*/ true).await;
|
||||
assert_eq!(
|
||||
direct_config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::Enabled
|
||||
);
|
||||
assert!(direct_config.permissions.network.is_none());
|
||||
assert!(managed_config.permissions.network.is_some());
|
||||
|
||||
let mut feature_disabled_config = direct_config.clone();
|
||||
feature_disabled_config
|
||||
.features
|
||||
.disable(Feature::TerminalBrowser)
|
||||
.expect("terminal-browser feature should be mutable in tests");
|
||||
let mut restricted_config = direct_config.clone();
|
||||
restricted_config
|
||||
.permissions
|
||||
.set_permission_profile(PermissionProfile::read_only())
|
||||
.expect("read-only permission profile should be allowed in tests");
|
||||
assert_eq!(
|
||||
restricted_config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::Restricted
|
||||
);
|
||||
|
||||
let cases = [
|
||||
(
|
||||
"embedded direct",
|
||||
&direct_config,
|
||||
ThreadParamsMode::Embedded,
|
||||
true,
|
||||
),
|
||||
(
|
||||
"remote direct",
|
||||
&direct_config,
|
||||
ThreadParamsMode::Remote,
|
||||
false,
|
||||
),
|
||||
(
|
||||
"feature disabled",
|
||||
&feature_disabled_config,
|
||||
ThreadParamsMode::Embedded,
|
||||
false,
|
||||
),
|
||||
(
|
||||
"network restricted",
|
||||
&restricted_config,
|
||||
ThreadParamsMode::Embedded,
|
||||
false,
|
||||
),
|
||||
(
|
||||
"managed network",
|
||||
&managed_config,
|
||||
ThreadParamsMode::Embedded,
|
||||
false,
|
||||
),
|
||||
];
|
||||
for (name, config, mode, expected) in cases {
|
||||
let params = thread_start_params_from_config(
|
||||
config, mode, /*remote_cwd_override*/ None, /*session_start_source*/ None,
|
||||
);
|
||||
assert_eq!(params.dynamic_tools.is_some(), expected, "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_params_can_mark_clear_source() {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
Reference in New Issue
Block a user