mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
This PR partially rebase `unified_exec` on the `exec-server` and adapt the `exec-server` accordingly. ## What changed in `exec-server` 1. Replaced the old "broadcast-driven; process-global" event model with process-scoped session events. The goal is to be able to have dedicated handler for each process. 2. Add to protocol contract to support explicit lifecycle status and stream ordering: - `WriteResponse` now returns `WriteStatus` (Accepted, UnknownProcess, StdinClosed, Starting) instead of a bool. - Added seq fields to output/exited notifications. - Added terminal process/closed notification. 3. Demultiplexed remote notifications into per-process channels. Same as for the event sys 4. Local and remote backends now both implement ExecBackend. 5. Local backend wraps internal process ID/operations into per-process ExecProcess objects. 6. Remote backend registers a session channel before launch and unregisters on failed launch. ## What changed in `unified_exec` 1. Added unified process-state model and backend-neutral process wrapper. This will probably disappear in the future, but it makes it easier to keep the work flowing on both side. - `UnifiedExecProcess` now handles both local PTY sessions and remote exec-server processes through a shared `ProcessHandle`. - Added `ProcessState` to track has_exited, exit_code, and terminal failure message consistently across backends. 2. Routed write and lifecycle handling through process-level methods. ## Some rationals 1. The change centralizes execution transport in exec-server while preserving policy and orchestration ownership in core, avoiding duplicated launch approval logic. This comes from internal discussion. 2. Session-scoped events remove coupling/cross-talk between processes and make stream ordering and terminal state explicit (seq, closed, failed). 3. The failure-path surfacing (remote launch failures, write failures, transport disconnects) makes command tool output and cleanup behavior deterministic ## Follow-ups: * Unify the concept of thread ID behind an obfuscated struct * FD handling * Full zsh-fork compatibility * Full network sandboxing compatibility * Handle ws disconnection
188 lines
5.2 KiB
Rust
188 lines
5.2 KiB
Rust
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
|
|
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
|
|
pub const INITIALIZE_METHOD: &str = "initialize";
|
|
pub const INITIALIZED_METHOD: &str = "initialized";
|
|
pub const EXEC_METHOD: &str = "process/start";
|
|
pub const EXEC_READ_METHOD: &str = "process/read";
|
|
pub const EXEC_WRITE_METHOD: &str = "process/write";
|
|
pub const EXEC_TERMINATE_METHOD: &str = "process/terminate";
|
|
pub const EXEC_OUTPUT_DELTA_METHOD: &str = "process/output";
|
|
pub const EXEC_EXITED_METHOD: &str = "process/exited";
|
|
pub const EXEC_CLOSED_METHOD: &str = "process/closed";
|
|
pub const FS_READ_FILE_METHOD: &str = "fs/readFile";
|
|
pub const FS_WRITE_FILE_METHOD: &str = "fs/writeFile";
|
|
pub const FS_CREATE_DIRECTORY_METHOD: &str = "fs/createDirectory";
|
|
pub const FS_GET_METADATA_METHOD: &str = "fs/getMetadata";
|
|
pub const FS_READ_DIRECTORY_METHOD: &str = "fs/readDirectory";
|
|
pub const FS_REMOVE_METHOD: &str = "fs/remove";
|
|
pub const FS_COPY_METHOD: &str = "fs/copy";
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(transparent)]
|
|
pub struct ByteChunk(#[serde(with = "base64_bytes")] pub Vec<u8>);
|
|
|
|
impl ByteChunk {
|
|
pub fn into_inner(self) -> Vec<u8> {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl From<Vec<u8>> for ByteChunk {
|
|
fn from(value: Vec<u8>) -> Self {
|
|
Self(value)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct InitializeParams {
|
|
pub client_name: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct InitializeResponse {}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecParams {
|
|
/// Client-chosen logical process handle scoped to this connection/session.
|
|
/// This is a protocol key, not an OS pid.
|
|
pub process_id: String,
|
|
pub argv: Vec<String>,
|
|
pub cwd: PathBuf,
|
|
pub env: HashMap<String, String>,
|
|
pub tty: bool,
|
|
pub arg0: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecResponse {
|
|
pub process_id: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ReadParams {
|
|
pub process_id: String,
|
|
pub after_seq: Option<u64>,
|
|
pub max_bytes: Option<usize>,
|
|
pub wait_ms: Option<u64>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ProcessOutputChunk {
|
|
pub seq: u64,
|
|
pub stream: ExecOutputStream,
|
|
pub chunk: ByteChunk,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ReadResponse {
|
|
pub chunks: Vec<ProcessOutputChunk>,
|
|
pub next_seq: u64,
|
|
pub exited: bool,
|
|
pub exit_code: Option<i32>,
|
|
pub closed: bool,
|
|
pub failure: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct WriteParams {
|
|
pub process_id: String,
|
|
pub chunk: ByteChunk,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum WriteStatus {
|
|
Accepted,
|
|
UnknownProcess,
|
|
StdinClosed,
|
|
Starting,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct WriteResponse {
|
|
pub status: WriteStatus,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct TerminateParams {
|
|
pub process_id: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct TerminateResponse {
|
|
pub running: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum ExecOutputStream {
|
|
Stdout,
|
|
Stderr,
|
|
Pty,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecOutputDeltaNotification {
|
|
pub process_id: String,
|
|
pub seq: u64,
|
|
pub stream: ExecOutputStream,
|
|
pub chunk: ByteChunk,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecExitedNotification {
|
|
pub process_id: String,
|
|
pub seq: u64,
|
|
pub exit_code: i32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecClosedNotification {
|
|
pub process_id: String,
|
|
pub seq: u64,
|
|
}
|
|
|
|
mod base64_bytes {
|
|
use super::BASE64_STANDARD;
|
|
use base64::Engine as _;
|
|
use serde::Deserialize;
|
|
use serde::Deserializer;
|
|
use serde::Serializer;
|
|
|
|
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
serializer.serialize_str(&BASE64_STANDARD.encode(bytes))
|
|
}
|
|
|
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let encoded = String::deserialize(deserializer)?;
|
|
BASE64_STANDARD
|
|
.decode(encoded)
|
|
.map_err(serde::de::Error::custom)
|
|
}
|
|
}
|