mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
## What changed Extract executor startup into `codex-exec-server-cli`, shared by `codex exec-server` and the new standalone binary. Preserve executor arguments, authentication, configuration overrides, and sandbox helper dispatch across both entry points. Build the standalone binary with `cargo build -p codex-exec-server-cli --bin codex-exec-server` and run it with `codex-exec-server --listen stdio`. ## Testing Add standalone CLI parsing and help coverage, plus an integration test for version reporting, stdio initialization, filesystem reads, process execution, and sandbox helper re-execution when supported. GitOrigin-RevId: ab3a81c6b91b6f45e4d72d8add2e01af8372e97f
216 lines
7.9 KiB
Rust
216 lines
7.9 KiB
Rust
#![cfg(test)]
|
|
|
|
use std::io::PipeWriter;
|
|
use std::io::Write;
|
|
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::*;
|
|
|
|
fn pipe() -> (PipeWriter, ExecutorCredentialPipe) {
|
|
let (reader, writer) = std::io::pipe().expect("create credential pipe");
|
|
let pipe = ExecutorCredentialPipe::spawn_with_reader("env-a".to_string(), move || reader);
|
|
(writer, pipe)
|
|
}
|
|
|
|
fn frame(environment_id: &str, token: &str, expires_at: u64) -> Vec<u8> {
|
|
let mut value = serde_json::to_vec(&serde_json::json!({
|
|
"version": 1,
|
|
"environment_id": environment_id,
|
|
"token": token,
|
|
"expires_at": expires_at,
|
|
}))
|
|
.expect("serialize frame");
|
|
value.push(b'\n');
|
|
value
|
|
}
|
|
|
|
async fn next_update(updates: &mut watch::Receiver<CredentialState>) {
|
|
tokio::time::timeout(Duration::from_secs(2), updates.changed())
|
|
.await
|
|
.expect("credential reader should publish")
|
|
.expect("reader should remain open");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn waits_for_initial_rotation_replaces_token_and_eof_clears_auth() {
|
|
let (mut writer, pipe) = pipe();
|
|
let provider = pipe.provider;
|
|
let mut updates = provider.current.clone();
|
|
let expires = unix_timestamp().expect("clock") + 60;
|
|
assert_eq!(
|
|
provider
|
|
.wait_for_initial_with_timeout(Duration::from_millis(5))
|
|
.await,
|
|
Err(CredentialError::InitialTimeout)
|
|
);
|
|
assert!(provider.apply_auth_headers(HeaderMap::new()).await.is_err());
|
|
assert!(provider.to_auth_headers().is_err());
|
|
writer
|
|
.write_all(&frame("env-a", "first-scoped-token", expires))
|
|
.expect("write");
|
|
provider.wait_for_initial().await.expect("initial token");
|
|
next_update(&mut updates).await;
|
|
let headers = provider
|
|
.apply_auth_headers(HeaderMap::new())
|
|
.await
|
|
.expect("headers");
|
|
assert_eq!(headers.len(), 1);
|
|
assert_eq!(headers[AUTHORIZATION], "Bearer first-scoped-token");
|
|
assert!(headers[AUTHORIZATION].is_sensitive());
|
|
assert!(!format!("{provider:?} {headers:?}").contains("first-scoped-token"));
|
|
assert!(!provider.refresh_after_unauthorized(headers.clone()).await);
|
|
|
|
writer
|
|
.write_all(&frame("env-a", "rotated-scoped-token", expires))
|
|
.expect("rotate");
|
|
next_update(&mut updates).await;
|
|
// A request sent before the parent update can retry with the replacement.
|
|
assert!(provider.refresh_after_unauthorized(headers).await);
|
|
let headers = provider
|
|
.apply_auth_headers(HeaderMap::new())
|
|
.await
|
|
.expect("headers");
|
|
assert_eq!(headers.len(), 1);
|
|
assert_eq!(headers[AUTHORIZATION], "Bearer rotated-scoped-token");
|
|
assert!(headers[AUTHORIZATION].is_sensitive());
|
|
assert!(!provider.refresh_after_unauthorized(headers.clone()).await);
|
|
|
|
drop(writer);
|
|
pipe.parent_closed.await.expect("reader must monitor EOF");
|
|
assert!(provider.apply_auth_headers(HeaderMap::new()).await.is_err());
|
|
assert!(!provider.refresh_after_unauthorized(headers).await);
|
|
assert_eq!(provider.check_pipe_failure(), Ok(()));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn malformed_rotations_latch_failure_and_clear_existing_credential() {
|
|
let expires = unix_timestamp().expect("clock") + 60;
|
|
let mut oversized = vec![b'x'; MAX_FRAME_BYTES];
|
|
oversized.push(b'\n');
|
|
for (bad_frame, error) in [
|
|
(
|
|
frame("env-b", "do-not-log-cross-env", expires),
|
|
CredentialError::WrongEnvironment,
|
|
),
|
|
(
|
|
frame("env-a", "do-not-log-expired", /*expires_at*/ 1),
|
|
CredentialError::Expired,
|
|
),
|
|
(
|
|
frame("env-a", "invalid\ntoken", expires),
|
|
CredentialError::InvalidFrame,
|
|
),
|
|
(
|
|
b"invalid do-not-log-malformed\n".to_vec(),
|
|
CredentialError::InvalidFrame,
|
|
),
|
|
(b"\n".to_vec(), CredentialError::InvalidFrame),
|
|
(oversized, CredentialError::FrameTooLarge),
|
|
] {
|
|
let (mut writer, pipe) = pipe();
|
|
writer
|
|
.write_all(&frame("env-a", "initial-scoped-token", expires))
|
|
.expect("initial frame");
|
|
pipe.provider
|
|
.wait_for_initial()
|
|
.await
|
|
.expect("initial credential");
|
|
writer.write_all(&bad_frame).expect("bad frame");
|
|
pipe.parent_closed
|
|
.await
|
|
.expect("bad frame terminates without EOF");
|
|
let auth_error = pipe
|
|
.provider
|
|
.apply_auth_headers(HeaderMap::new())
|
|
.await
|
|
.expect_err("reject auth");
|
|
assert_eq!(pipe.provider.check_pipe_failure(), Err(error));
|
|
assert!(
|
|
!pipe
|
|
.provider
|
|
.refresh_after_unauthorized(HeaderMap::new())
|
|
.await
|
|
);
|
|
assert!(!auth_error.to_string().contains("do-not-log"));
|
|
assert!(!auth_error.to_string().contains("initial-scoped-token"));
|
|
let _ = writer.write_all(&frame("env-a", "later-scoped-token", expires));
|
|
assert_eq!(pipe.provider.check_pipe_failure(), Err(error));
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn eof_before_a_complete_frame_never_authenticates() {
|
|
let expires = unix_timestamp().expect("clock") + 60;
|
|
for (bytes, expected) in [
|
|
(Vec::new(), CredentialError::Closed),
|
|
(
|
|
frame("env-a", "scoped-token", expires)
|
|
.split_last()
|
|
.expect("newline")
|
|
.1
|
|
.to_vec(),
|
|
CredentialError::InvalidFrame,
|
|
),
|
|
] {
|
|
let (mut writer, pipe) = pipe();
|
|
if !bytes.is_empty() {
|
|
writer.write_all(&bytes).expect("partial frame");
|
|
}
|
|
drop(writer);
|
|
pipe.parent_closed.await.expect("reader monitors EOF");
|
|
assert_eq!(pipe.provider.wait_for_initial().await, Err(expected));
|
|
assert!(
|
|
pipe.provider
|
|
.apply_auth_headers(HeaderMap::new())
|
|
.await
|
|
.is_err()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn expiry_is_checked_again_on_every_request() {
|
|
let expired = unix_timestamp().expect("clock");
|
|
let (updates, current) = watch::channel(CredentialState::Active {
|
|
token: HeaderValue::from_static("unused-expired-token"),
|
|
expires_at: expired,
|
|
});
|
|
let provider = ExecutorCredentialProvider { current };
|
|
assert_eq!(
|
|
provider.wait_for_initial().await,
|
|
Err(CredentialError::Expired)
|
|
);
|
|
assert!(provider.apply_auth_headers(HeaderMap::new()).await.is_err());
|
|
assert!(!provider.refresh_after_unauthorized(HeaderMap::new()).await);
|
|
updates.send_replace(CredentialState::Active {
|
|
token: HeaderValue::from_static("new-scoped-token"),
|
|
expires_at: expired + 60,
|
|
});
|
|
let headers = provider
|
|
.apply_auth_headers(HeaderMap::new())
|
|
.await
|
|
.expect("rotation restores auth");
|
|
assert_eq!(headers[AUTHORIZATION], "Bearer new-scoped-token");
|
|
}
|
|
|
|
#[test]
|
|
fn parser_rejects_unsupported_schema_without_exposing_input() {
|
|
let expires = unix_timestamp().expect("clock") + 60;
|
|
for invalid in [
|
|
serde_json::json!({"version": 2, "environment_id": "env-a", "token": "secret", "expires_at": expires}),
|
|
serde_json::json!({"version": 1, "environment_id": "env-a", "token": "secret", "expires_at": expires, "unexpected": true}),
|
|
serde_json::json!({"version": 1, "environment_id": "env-a", "token": "secret", "expires_at": -1}),
|
|
serde_json::json!({"version": 1, "environment_id": "env-a", "token": " ", "expires_at": expires}),
|
|
serde_json::json!({"version": 1, "environment_id": "env-a", "token": "secret token", "expires_at": expires}),
|
|
serde_json::json!({"version": 1, "environment_id": "env-a", "token": "secret-☃", "expires_at": expires}),
|
|
] {
|
|
let serialized = serde_json::to_vec(&invalid).expect("serialize test input");
|
|
let error = parse_frame(&serialized, "env-a")
|
|
.err()
|
|
.expect("invalid schema");
|
|
assert_eq!(error, CredentialError::InvalidFrame);
|
|
assert!(!error.to_string().contains("secret"));
|
|
}
|
|
}
|