mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Add installed voice host lifecycle support (#41902)
## What changed - Add `VoiceHost` to resolve the packaged voice helper, launch it with an allowlisted environment, perform the protocol handshake, and enforce bounded shutdown and process cleanup. - Preserve native executable path encoding in the pipe process APIs. - Add `third_party/voice/assemble_package.py` to create a fresh package copy containing a target-compatible helper and a provenance manifest with file hashes. ## Testing - Cover installed helper lifecycle, build matching, missing and symlinked helpers, non-UTF-8 package paths, environment filtering, package validation, target pairing, and failure cleanup. GitOrigin-RevId: f893074b36ae6bb9bcedc00fbe7af6bb72745f4c
This commit is contained in:
1 parent
f88ff940c0
commit
13bc770eaf
12 files changed
+493
-10
No files matched your search
@@ -0,0 +1,153 @@
|
||||
//! Resolve only the installed helper and retain the existing owned-process cleanup guard.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::ensure;
|
||||
use codex_install_context::CodexPackageLayout;
|
||||
use codex_utils_pty::ProcessHandle;
|
||||
use codex_utils_pty::SpawnedProcess;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::MAX_FRAME_BYTES;
|
||||
use crate::Message;
|
||||
use crate::decode_frame;
|
||||
use crate::encode_frame;
|
||||
|
||||
const DEADLINE: Duration = Duration::from_secs(/*secs*/ 5);
|
||||
|
||||
/// Owns one helper. Dropping it terminates the process and leaves its waiter to reap it.
|
||||
/// A successful handshake establishes compatibility only, not an active audio session.
|
||||
pub struct VoiceHost {
|
||||
process: ProcessHandle,
|
||||
output: mpsc::Receiver<Vec<u8>>,
|
||||
exit: oneshot::Receiver<i32>,
|
||||
}
|
||||
|
||||
impl VoiceHost {
|
||||
pub async fn connect(package: &CodexPackageLayout, build_commit: &str) -> Result<Self> {
|
||||
let root = package.package_dir.as_path().canonicalize()?;
|
||||
let name = if cfg!(windows) {
|
||||
"codex-voice-host.exe"
|
||||
} else {
|
||||
"codex-voice-host"
|
||||
};
|
||||
let path = root.join("codex-resources/voice/bin").join(name);
|
||||
ensure!(
|
||||
path.canonicalize()? == path,
|
||||
"voice helper must be inside the physical package"
|
||||
);
|
||||
let environment = child_environment(std::env::vars_os());
|
||||
let SpawnedProcess {
|
||||
session,
|
||||
stdout_rx,
|
||||
stderr_rx,
|
||||
exit_rx,
|
||||
} = codex_utils_pty::spawn_pipe_process(
|
||||
&path,
|
||||
&[],
|
||||
&root,
|
||||
&environment,
|
||||
/*arg0*/ &None,
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
drop(stderr_rx); // Drain and discard diagnostics rather than logging untyped child output.
|
||||
let mut host = Self {
|
||||
process: session,
|
||||
output: stdout_rx,
|
||||
exit: exit_rx,
|
||||
};
|
||||
host.exchange(
|
||||
Message::Hello {
|
||||
protocol: 1,
|
||||
build_commit: build_commit.to_owned(),
|
||||
},
|
||||
Message::Ready {},
|
||||
)
|
||||
.await?;
|
||||
Ok(host)
|
||||
}
|
||||
|
||||
pub async fn close(mut self) -> Result<()> {
|
||||
let result = self.exchange(Message::Close {}, Message::Closed {}).await;
|
||||
if result.is_err() {
|
||||
self.process.terminate();
|
||||
}
|
||||
let code = timeout(DEADLINE, &mut self.exit).await??;
|
||||
result?;
|
||||
ensure!(code == 0, "voice helper failed during shutdown");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn exchange(&mut self, request: Message, expected: Message) -> Result<()> {
|
||||
timeout(DEADLINE, async {
|
||||
self.process
|
||||
.writer_sender()
|
||||
.send(encode_frame(&request)?)
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("voice helper input closed"))?;
|
||||
let mut frame = Vec::new();
|
||||
loop {
|
||||
let chunk = self
|
||||
.output
|
||||
.recv()
|
||||
.await
|
||||
.context("voice helper output closed")?;
|
||||
ensure!(
|
||||
frame.len() + chunk.len() <= MAX_FRAME_BYTES + 4,
|
||||
"voice helper output exceeds limit"
|
||||
);
|
||||
frame.extend(chunk);
|
||||
if let Some(response) = decode_frame(&frame)? {
|
||||
ensure!(response == expected, "unexpected voice helper response");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
fn child_environment(vars: impl Iterator<Item = (OsString, OsString)>) -> HashMap<String, String> {
|
||||
vars.filter_map(|(key, value)| {
|
||||
let key = key.into_string().ok()?;
|
||||
matches!(
|
||||
key.to_ascii_uppercase().as_str(),
|
||||
"SYSTEMROOT"
|
||||
| "WINDIR"
|
||||
| "HOME"
|
||||
| "USERPROFILE"
|
||||
| "LOCALAPPDATA"
|
||||
| "APPDATA"
|
||||
| "TEMP"
|
||||
| "TMP"
|
||||
| "TMPDIR"
|
||||
| "XDG_RUNTIME_DIR"
|
||||
| "PULSE_SERVER"
|
||||
| "PULSE_COOKIE"
|
||||
| "PIPEWIRE_REMOTE"
|
||||
| "DBUS_SESSION_BUS_ADDRESS"
|
||||
| "HTTP_PROXY"
|
||||
| "HTTPS_PROXY"
|
||||
| "ALL_PROXY"
|
||||
| "NO_PROXY"
|
||||
| "SSL_CERT_FILE"
|
||||
| "SSL_CERT_DIR"
|
||||
| "REQUESTS_CA_BUNDLE"
|
||||
| "CURL_CA_BUNDLE"
|
||||
)
|
||||
.then(|| Some((key, value.into_string().ok()?)))
|
||||
.flatten()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "client_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,30 @@
|
||||
//! Child environment admission excludes loader hooks, plugin search paths, and app credentials.
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::child_environment;
|
||||
|
||||
#[test]
|
||||
fn forwards_only_explicit_device_network_and_os_inputs() {
|
||||
let input = [
|
||||
("SystemRoot", "C:\\Windows"),
|
||||
("https_proxy", "http://proxy"),
|
||||
("HOME", "/home/user"),
|
||||
("LD_PRELOAD", "loader"),
|
||||
("DYLD_INSERT_LIBRARIES", "loader"),
|
||||
("PATH", "project"),
|
||||
("GST_PLUGIN_PATH", "plugins"),
|
||||
("OPENAI_API_KEY", "secret"),
|
||||
];
|
||||
assert_eq!(
|
||||
child_environment(
|
||||
input
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key.into(), value.into()))
|
||||
),
|
||||
input[..3]
|
||||
.iter()
|
||||
.map(|(key, value)| (key.to_string(), value.to_string()))
|
||||
.collect()
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
mod client;
|
||||
mod protocol;
|
||||
|
||||
pub use client::VoiceHost;
|
||||
pub use protocol::MAX_FRAME_BYTES;
|
||||
pub use protocol::Message;
|
||||
pub use protocol::decode_frame;
|
||||
|
||||
Reference in new issue
Block a user