mirror of
https://github.com/openai/codex.git
synced 2026-04-29 08:56:38 +00:00
Add thread/shellCommand to app server API surface (#14988)
This PR adds a new `thread/shellCommand` app server API so clients can implement `!` shell commands. These commands are executed within the sandbox, and the command text and output are visible to the model. The internal implementation mirrors the current TUI `!` behavior. - persist shell command execution as `CommandExecution` thread items, including source and formatted output metadata - bridge live and replayed app-server command execution events back into the existing `tui_app_server` exec rendering path This PR also wires `tui_app_server` to submit `!` commands through the new API.
This commit is contained in:
@@ -52,6 +52,7 @@ use codex_protocol::protocol::AgentStatus as CoreAgentStatus;
|
||||
use codex_protocol::protocol::AskForApproval as CoreAskForApproval;
|
||||
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
|
||||
use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot;
|
||||
use codex_protocol::protocol::ExecCommandSource as CoreExecCommandSource;
|
||||
use codex_protocol::protocol::ExecCommandStatus as CoreExecCommandStatus;
|
||||
use codex_protocol::protocol::GranularApprovalConfig as CoreGranularApprovalConfig;
|
||||
use codex_protocol::protocol::GuardianRiskLevel as CoreGuardianRiskLevel;
|
||||
@@ -92,6 +93,7 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_with::serde_as;
|
||||
use thiserror::Error;
|
||||
use ts_rs::TS;
|
||||
|
||||
@@ -2871,6 +2873,23 @@ pub struct ThreadCompactStartParams {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadCompactStartResponse {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadShellCommandParams {
|
||||
pub thread_id: String,
|
||||
/// Shell command string evaluated by the thread's configured shell.
|
||||
/// Unlike `command/exec`, this intentionally preserves shell syntax
|
||||
/// such as pipes, redirects, and quoting. This runs unsandboxed with full
|
||||
/// access rather than inheriting the thread sandbox policy.
|
||||
pub command: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadShellCommandResponse {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -4137,6 +4156,8 @@ pub enum ThreadItem {
|
||||
cwd: PathBuf,
|
||||
/// Identifier for the underlying PTY process (when available).
|
||||
process_id: Option<String>,
|
||||
#[serde(default)]
|
||||
source: CommandExecutionSource,
|
||||
status: CommandExecutionStatus,
|
||||
/// A best-effort parsing of the command to understand the action(s) it will perform.
|
||||
/// This returns a list of CommandAction objects because a single shell command may
|
||||
@@ -4417,6 +4438,17 @@ impl From<&CoreExecCommandStatus> for CommandExecutionStatus {
|
||||
}
|
||||
}
|
||||
|
||||
v2_enum_from_core! {
|
||||
#[derive(Default)]
|
||||
pub enum CommandExecutionSource from CoreExecCommandSource {
|
||||
#[default]
|
||||
Agent,
|
||||
UserShell,
|
||||
UnifiedExecStartup,
|
||||
UnifiedExecInteraction,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -4863,6 +4895,7 @@ pub struct TerminalInteractionNotification {
|
||||
pub stdin: String,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -6313,6 +6346,40 @@ mod tests {
|
||||
assert_eq!(decoded, params);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_shell_command_params_round_trip() {
|
||||
let params = ThreadShellCommandParams {
|
||||
thread_id: "thr_123".to_string(),
|
||||
command: "printf 'hello world\\n'".to_string(),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(¶ms).expect("serialize thread/shellCommand params");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"threadId": "thr_123",
|
||||
"command": "printf 'hello world\\n'",
|
||||
})
|
||||
);
|
||||
|
||||
let decoded = serde_json::from_value::<ThreadShellCommandParams>(value)
|
||||
.expect("deserialize thread/shellCommand params");
|
||||
assert_eq!(decoded, params);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_shell_command_response_round_trip() {
|
||||
let response = ThreadShellCommandResponse {};
|
||||
|
||||
let value =
|
||||
serde_json::to_value(&response).expect("serialize thread/shellCommand response");
|
||||
assert_eq!(value, json!({}));
|
||||
|
||||
let decoded = serde_json::from_value::<ThreadShellCommandResponse>(value)
|
||||
.expect("deserialize thread/shellCommand response");
|
||||
assert_eq!(decoded, response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_exec_params_default_optional_streaming_flags() {
|
||||
let params = serde_json::from_value::<CommandExecParams>(json!({
|
||||
@@ -6607,6 +6674,32 @@ mod tests {
|
||||
assert_eq!(decoded, notification);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_execution_output_delta_round_trips() {
|
||||
let notification = CommandExecutionOutputDeltaNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "item-1".to_string(),
|
||||
delta: "\u{fffd}a\n".to_string(),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(¬ification)
|
||||
.expect("serialize item/commandExecution/outputDelta notification");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"threadId": "thread-1",
|
||||
"turnId": "turn-1",
|
||||
"itemId": "item-1",
|
||||
"delta": "\u{fffd}a\n",
|
||||
})
|
||||
);
|
||||
|
||||
let decoded = serde_json::from_value::<CommandExecutionOutputDeltaNotification>(value)
|
||||
.expect("deserialize round-trip");
|
||||
assert_eq!(decoded, notification);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_policy_round_trips_external_sandbox_network_access() {
|
||||
let v2_policy = SandboxPolicy::ExternalSandbox {
|
||||
|
||||
Reference in New Issue
Block a user