mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Wire remote app-server auth through the client (#14853)
For app-server websocket auth, support the two server-side mechanisms from PR #14847: - `--ws-auth capability-token --ws-token-file /abs/path` - `--ws-auth signed-bearer-token --ws-shared-secret-file /abs/path` with optional `--ws-issuer`, `--ws-audience`, and `--ws-max-clock-skew-seconds` On the client side, add interactive remote support via: - `--remote ws://host:port` or `--remote wss://host:port` - `--remote-auth-token-env <ENV_VAR>` Codex reads the bearer token from the named environment variable and sends it as `Authorization: Bearer <token>` during the websocket handshake. Remote auth tokens are only allowed for `wss://` URLs or loopback `ws://` URLs. Testing: - tested both auth methods manually to confirm connection success and rejection for both auth types
This commit is contained in:
committed by
GitHub
Unverified
parent
b565f05d79
commit
1ff39b6fa8
@@ -959,6 +959,7 @@ pub(crate) struct App {
|
||||
pub(crate) feedback: codex_feedback::CodexFeedback,
|
||||
feedback_audience: FeedbackAudience,
|
||||
remote_app_server_url: Option<String>,
|
||||
remote_app_server_auth_token: Option<String>,
|
||||
/// Set when the user confirms an update; propagated on exit.
|
||||
pub(crate) pending_update_action: Option<UpdateAction>,
|
||||
|
||||
@@ -3124,6 +3125,7 @@ impl App {
|
||||
is_first_run: bool,
|
||||
should_prompt_windows_sandbox_nux_at_startup: bool,
|
||||
remote_app_server_url: Option<String>,
|
||||
remote_app_server_auth_token: Option<String>,
|
||||
) -> Result<AppExitInfo> {
|
||||
use tokio_stream::StreamExt;
|
||||
let (app_event_tx, mut app_event_rx) = unbounded_channel();
|
||||
@@ -3335,6 +3337,7 @@ impl App {
|
||||
feedback: feedback.clone(),
|
||||
feedback_audience,
|
||||
remote_app_server_url,
|
||||
remote_app_server_auth_token,
|
||||
pending_update_action: None,
|
||||
pending_shutdown_exit_thread_id: None,
|
||||
windows_sandbox: WindowsSandboxState::default(),
|
||||
@@ -3583,7 +3586,10 @@ impl App {
|
||||
let picker_app_server = match crate::start_app_server_for_picker(
|
||||
&self.config,
|
||||
&match self.remote_app_server_url.clone() {
|
||||
Some(websocket_url) => crate::AppServerTarget::Remote(websocket_url),
|
||||
Some(websocket_url) => crate::AppServerTarget::Remote {
|
||||
websocket_url,
|
||||
auth_token: self.remote_app_server_auth_token.clone(),
|
||||
},
|
||||
None => crate::AppServerTarget::Embedded,
|
||||
},
|
||||
)
|
||||
@@ -8121,6 +8127,7 @@ guardian_approval = true
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
feedback_audience: FeedbackAudience::External,
|
||||
remote_app_server_url: None,
|
||||
remote_app_server_auth_token: None,
|
||||
pending_update_action: None,
|
||||
pending_shutdown_exit_thread_id: None,
|
||||
windows_sandbox: WindowsSandboxState::default(),
|
||||
@@ -8173,6 +8180,7 @@ guardian_approval = true
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
feedback_audience: FeedbackAudience::External,
|
||||
remote_app_server_url: None,
|
||||
remote_app_server_auth_token: None,
|
||||
pending_update_action: None,
|
||||
pending_shutdown_exit_thread_id: None,
|
||||
windows_sandbox: WindowsSandboxState::default(),
|
||||
|
||||
@@ -284,7 +284,10 @@ async fn start_embedded_app_server(
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum AppServerTarget {
|
||||
Embedded,
|
||||
Remote(String),
|
||||
Remote {
|
||||
websocket_url: String,
|
||||
auth_token: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
fn remote_addr_has_explicit_port(addr: &str, parsed: &Url) -> bool {
|
||||
@@ -316,6 +319,16 @@ fn remote_addr_has_explicit_port(addr: &str, parsed: &Url) -> bool {
|
||||
host_and_port == format!("{expected_host}:{explicit_default_port}")
|
||||
}
|
||||
|
||||
fn websocket_url_supports_auth_token(parsed: &Url) -> bool {
|
||||
match (parsed.scheme(), parsed.host()) {
|
||||
("wss", Some(_)) => true,
|
||||
("ws", Some(url::Host::Domain(domain))) => domain.eq_ignore_ascii_case("localhost"),
|
||||
("ws", Some(url::Host::Ipv4(addr))) => addr.is_loopback(),
|
||||
("ws", Some(url::Host::Ipv6(addr))) => addr.is_loopback(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_remote_addr(addr: &str) -> color_eyre::Result<String> {
|
||||
let parsed = match Url::parse(addr) {
|
||||
Ok(parsed) => parsed,
|
||||
@@ -340,9 +353,24 @@ pub fn normalize_remote_addr(addr: &str) -> color_eyre::Result<String> {
|
||||
);
|
||||
}
|
||||
|
||||
async fn connect_remote_app_server(websocket_url: String) -> color_eyre::Result<AppServerClient> {
|
||||
fn validate_remote_auth_token_transport(websocket_url: &str) -> color_eyre::Result<()> {
|
||||
let parsed = Url::parse(websocket_url).map_err(color_eyre::Report::new)?;
|
||||
if websocket_url_supports_auth_token(&parsed) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
color_eyre::eyre::bail!(
|
||||
"remote auth tokens require `wss://` or loopback `ws://` URLs; got `{websocket_url}`"
|
||||
)
|
||||
}
|
||||
|
||||
async fn connect_remote_app_server(
|
||||
websocket_url: String,
|
||||
auth_token: Option<String>,
|
||||
) -> color_eyre::Result<AppServerClient> {
|
||||
let app_server = RemoteAppServerClient::connect(RemoteAppServerConnectArgs {
|
||||
websocket_url,
|
||||
auth_token,
|
||||
client_name: "codex-tui".to_string(),
|
||||
client_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
experimental_api: true,
|
||||
@@ -374,9 +402,10 @@ async fn start_app_server(
|
||||
)
|
||||
.await
|
||||
.map(AppServerClient::InProcess),
|
||||
AppServerTarget::Remote(websocket_url) => {
|
||||
connect_remote_app_server(websocket_url.clone()).await
|
||||
}
|
||||
AppServerTarget::Remote {
|
||||
websocket_url,
|
||||
auth_token,
|
||||
} => connect_remote_app_server(websocket_url.clone(), auth_token.clone()).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,11 +619,18 @@ pub async fn run_main(
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
loader_overrides: LoaderOverrides,
|
||||
remote: Option<String>,
|
||||
remote_auth_token: Option<String>,
|
||||
) -> std::io::Result<AppExitInfo> {
|
||||
let remote_url = remote;
|
||||
if let (Some(websocket_url), Some(_)) = (remote_url.as_deref(), remote_auth_token.as_ref()) {
|
||||
validate_remote_auth_token_transport(websocket_url).map_err(std::io::Error::other)?;
|
||||
}
|
||||
let app_server_target = remote_url
|
||||
.clone()
|
||||
.map(AppServerTarget::Remote)
|
||||
.map(|websocket_url| AppServerTarget::Remote {
|
||||
websocket_url,
|
||||
auth_token: remote_auth_token.clone(),
|
||||
})
|
||||
.unwrap_or(AppServerTarget::Embedded);
|
||||
let (sandbox_mode, approval_policy) = if cli.full_auto {
|
||||
(
|
||||
@@ -904,6 +940,7 @@ pub async fn run_main(
|
||||
cloud_requirements,
|
||||
feedback,
|
||||
remote_url,
|
||||
remote_auth_token,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| std::io::Error::other(err.to_string()))
|
||||
@@ -921,8 +958,9 @@ async fn run_ratatui_app(
|
||||
mut cloud_requirements: CloudRequirementsLoader,
|
||||
feedback: codex_feedback::CodexFeedback,
|
||||
remote_url: Option<String>,
|
||||
remote_auth_token: Option<String>,
|
||||
) -> color_eyre::Result<AppExitInfo> {
|
||||
let remote_mode = matches!(&app_server_target, AppServerTarget::Remote(_));
|
||||
let remote_mode = matches!(&app_server_target, AppServerTarget::Remote { .. });
|
||||
color_eyre::install()?;
|
||||
|
||||
tooltips::announcement::prewarm();
|
||||
@@ -1326,6 +1364,7 @@ async fn run_ratatui_app(
|
||||
should_show_trust_screen, // Proxy to: is it a first run in this directory?
|
||||
should_prompt_windows_sandbox_nux_at_startup,
|
||||
remote_url,
|
||||
remote_auth_token,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1724,6 +1763,32 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_auth_token_transport_accepts_loopback_ws() {
|
||||
validate_remote_auth_token_transport("ws://127.0.0.1:4500/")
|
||||
.expect("loopback ws should be allowed for auth tokens");
|
||||
validate_remote_auth_token_transport("ws://localhost:4500/")
|
||||
.expect("localhost ws should be allowed for auth tokens");
|
||||
validate_remote_auth_token_transport("ws://[::1]:4500/")
|
||||
.expect("ipv6 loopback ws should be allowed for auth tokens");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_auth_token_transport_accepts_secure_wss() {
|
||||
validate_remote_auth_token_transport("wss://example.com:443/")
|
||||
.expect("wss should be allowed for auth tokens");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_auth_token_transport_rejects_non_loopback_ws() {
|
||||
let err = validate_remote_auth_token_transport("ws://example.com:4500/")
|
||||
.expect_err("non-loopback ws should be rejected for auth tokens");
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("remote auth tokens require `wss://` or loopback `ws://` URLs")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn latest_session_lookup_params_keep_local_filters_for_embedded_sessions()
|
||||
-> std::io::Result<()> {
|
||||
|
||||
@@ -27,6 +27,7 @@ fn main() -> anyhow::Result<()> {
|
||||
arg0_paths,
|
||||
codex_core::config_loader::LoaderOverrides::default(),
|
||||
/*remote*/ None,
|
||||
/*remote_auth_token*/ None,
|
||||
)
|
||||
.await?;
|
||||
let token_usage = exit_info.token_usage;
|
||||
|
||||
Reference in New Issue
Block a user