Skip to content

Commit c549481

Browse files
authored
feat: introduce responses-api-proxy (#4246)
Details are in `responses-api-proxy/README.md`, but the key contribution of this PR is a new subcommand, `codex responses-api-proxy`, which reads the auth token for use with the OpenAI Responses API from `stdin` at startup and then proxies `POST` requests to `/v1/responses` over to `https://api.openai.com/v1/responses`, injecting the auth token as part of the `Authorization` header. The expectation is that `codex responses-api-proxy` is launched by a privileged user who has access to the auth token so that it can be used by unprivileged users of the Codex CLI on the same host. If the client only has one user account with `sudo`, one option is to: - run `sudo codex responses-api-proxy --http-shutdown --server-info /tmp/server-info.json` to start the server - record the port written to `/tmp/server-info.json` - relinquish their `sudo` privileges (which is irreversible!) like so: ``` sudo deluser $USER sudo || sudo gpasswd -d $USER sudo || true ``` - use `codex` with the proxy (see `README.md`) - when done, make a `GET` request to the server using the `PORT` from `server-info.json` to shut it down: ```shell curl --fail --silent --show-error "http://127.0.0.1:$PORT/shutdown" ``` To protect the auth token, we: - allocate a 1024 byte buffer on the stack and write `"Bearer "` into it to start - we then read from `stdin`, copying to the contents into the buffer after the prefix - after verifying the input looks good, we create a `String` from that buffer (so the data is now on the heap) - we zero out the stack-allocated buffer using https://crates.io/crates/zeroize so it is not optimized away by the compiler - we invoke `.leak()` on the `String` so we can treat its contents as a `&'static str`, as it will live for the rest of the processs - on UNIX, we `mlock(2)` the memory backing the `&'static str` - when using the `&'static str` when building an HTTP request, we use `HeaderValue::from_static()` to avoid copying the `&str` - we also invoke `.set_sensitive(true)` on the `HeaderValue`, which in theory indicates to other parts of the HTTP stack that the header should be treated with "special care" to avoid leakage: https://github.com/hyperium/http/blob/439d1c50d71e3be3204b6c4a1bf2255ed78e1f93/src/header/value.rs#L346-L376
1 parent 8797145 commit c549481

File tree

9 files changed

+632
-1
lines changed

9 files changed

+632
-1
lines changed

codex-rs/Cargo.lock

Lines changed: 136 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ members = [
1818
"ollama",
1919
"protocol",
2020
"protocol-ts",
21+
"responses-api-proxy",
2122
"tui",
2223
"utils/readiness",
2324
]
@@ -49,6 +50,7 @@ codex-mcp-server = { path = "mcp-server" }
4950
codex-ollama = { path = "ollama" }
5051
codex-protocol = { path = "protocol" }
5152
codex-protocol-ts = { path = "protocol-ts" }
53+
codex-responses-api-proxy = { path = "responses-api-proxy" }
5254
codex-tui = { path = "tui" }
5355
codex-utils-readiness = { path = "utils/readiness" }
5456
core_test_support = { path = "core/tests/common" }
@@ -152,6 +154,7 @@ webbrowser = "1.0"
152154
which = "6"
153155
wildmatch = "2.5.0"
154156
wiremock = "0.6"
157+
zeroize = "1.8.1"
155158

156159
[workspace.lints]
157160
rust = {}

codex-rs/cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ codex-login = { workspace = true }
2727
codex-mcp-server = { workspace = true }
2828
codex-protocol = { workspace = true }
2929
codex-protocol-ts = { workspace = true }
30+
codex-responses-api-proxy = { workspace = true }
3031
codex-tui = { workspace = true }
3132
ctor = { workspace = true }
3233
owo-colors = { workspace = true }

codex-rs/cli/src/main.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use anyhow::Context;
12
use clap::CommandFactory;
23
use clap::Parser;
34
use clap_complete::Shell;
@@ -14,6 +15,7 @@ use codex_cli::login::run_logout;
1415
use codex_cli::proto;
1516
use codex_common::CliConfigOverrides;
1617
use codex_exec::Cli as ExecCli;
18+
use codex_responses_api_proxy::Args as ResponsesApiProxyArgs;
1719
use codex_tui::AppExitInfo;
1820
use codex_tui::Cli as TuiCli;
1921
use owo_colors::OwoColorize;
@@ -86,6 +88,10 @@ enum Subcommand {
8688
/// Internal: generate TypeScript protocol bindings.
8789
#[clap(hide = true)]
8890
GenerateTs(GenerateTsCommand),
91+
92+
/// Internal: run the responses API proxy.
93+
#[clap(hide = true)]
94+
ResponsesApiProxy(ResponsesApiProxyArgs),
8995
}
9096

9197
#[derive(Debug, Parser)]
@@ -341,6 +347,11 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
341347
Some(Subcommand::GenerateTs(gen_cli)) => {
342348
codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?;
343349
}
350+
Some(Subcommand::ResponsesApiProxy(args)) => {
351+
tokio::task::spawn_blocking(move || codex_responses_api_proxy::run_main(args))
352+
.await
353+
.context("responses-api-proxy blocking task panicked")??;
354+
}
344355
}
345356

346357
Ok(())
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
edition = "2024"
3+
name = "codex-responses-api-proxy"
4+
version = { workspace = true }
5+
6+
[lib]
7+
name = "codex_responses_api_proxy"
8+
path = "src/lib.rs"
9+
10+
[[bin]]
11+
name = "responses-api-proxy"
12+
path = "src/main.rs"
13+
14+
[lints]
15+
workspace = true
16+
17+
[dependencies]
18+
anyhow = { workspace = true }
19+
clap = { workspace = true, features = ["derive"] }
20+
codex-arg0 = { workspace = true }
21+
libc = { workspace = true }
22+
reqwest = { workspace = true, features = ["blocking", "json", "rustls-tls"] }
23+
serde = { workspace = true, features = ["derive"] }
24+
serde_json = { workspace = true }
25+
tiny_http = { workspace = true }
26+
tokio = { workspace = true }
27+
zeroize = { workspace = true }

0 commit comments

Comments
 (0)