Smooth codex login (#1155)
* Add codex setup helper Pass exit result Move codex setup to routes FIx fmt Fix finalize * Rename scriptcontext (vibe-kanban 79207902) Rename the gh cli script context to something more general and use it for installs in crates/server/src/routes/task_attempts/cursor_setup.rs Rename scriptcontext (vibe-kanban 79207902) Rename the gh cli script context to something more general and use it for installs in crates/server/src/routes/task_attempts/cursor_setup.rs Fmt * Fix missing overrides for codex
This commit is contained in:
@@ -23,7 +23,7 @@ pub enum ScriptContext {
|
|||||||
SetupScript,
|
SetupScript,
|
||||||
CleanupScript,
|
CleanupScript,
|
||||||
DevServer,
|
DevServer,
|
||||||
GithubCliSetupScript,
|
ToolInstallScript,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use workspace_utils::stream_lines::LinesStreamExt;
|
|||||||
use super::{AcpClient, SessionManager};
|
use super::{AcpClient, SessionManager};
|
||||||
use crate::{
|
use crate::{
|
||||||
command::CommandParts,
|
command::CommandParts,
|
||||||
executors::{ExecutorError, SpawnedChild, acp::AcpEvent},
|
executors::{ExecutorError, ExecutorExitResult, SpawnedChild, acp::AcpEvent},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Reusable harness for ACP-based conns (Gemini, Qwen, etc.)
|
/// Reusable harness for ACP-based conns (Gemini, Qwen, etc.)
|
||||||
@@ -68,7 +68,7 @@ impl AcpAgentHarness {
|
|||||||
|
|
||||||
let mut child = command.group_spawn()?;
|
let mut child = command.group_spawn()?;
|
||||||
|
|
||||||
let (exit_tx, exit_rx) = tokio::sync::oneshot::channel::<()>();
|
let (exit_tx, exit_rx) = tokio::sync::oneshot::channel::<ExecutorExitResult>();
|
||||||
Self::bootstrap_acp_connection(
|
Self::bootstrap_acp_connection(
|
||||||
&mut child,
|
&mut child,
|
||||||
current_dir.to_path_buf(),
|
current_dir.to_path_buf(),
|
||||||
@@ -105,7 +105,7 @@ impl AcpAgentHarness {
|
|||||||
|
|
||||||
let mut child = command.group_spawn()?;
|
let mut child = command.group_spawn()?;
|
||||||
|
|
||||||
let (exit_tx, exit_rx) = tokio::sync::oneshot::channel::<()>();
|
let (exit_tx, exit_rx) = tokio::sync::oneshot::channel::<ExecutorExitResult>();
|
||||||
Self::bootstrap_acp_connection(
|
Self::bootstrap_acp_connection(
|
||||||
&mut child,
|
&mut child,
|
||||||
current_dir.to_path_buf(),
|
current_dir.to_path_buf(),
|
||||||
@@ -127,7 +127,7 @@ impl AcpAgentHarness {
|
|||||||
cwd: PathBuf,
|
cwd: PathBuf,
|
||||||
existing_session: Option<String>,
|
existing_session: Option<String>,
|
||||||
prompt: String,
|
prompt: String,
|
||||||
exit_signal: Option<tokio::sync::oneshot::Sender<()>>,
|
exit_signal: Option<tokio::sync::oneshot::Sender<ExecutorExitResult>>,
|
||||||
session_namespace: String,
|
session_namespace: String,
|
||||||
) -> Result<(), ExecutorError> {
|
) -> Result<(), ExecutorError> {
|
||||||
// Take child's stdio for ACP wiring
|
// Take child's stdio for ACP wiring
|
||||||
@@ -387,7 +387,7 @@ impl AcpAgentHarness {
|
|||||||
}
|
}
|
||||||
// Notify container of completion
|
// Notify container of completion
|
||||||
if let Some(tx) = exit_signal_tx.take() {
|
if let Some(tx) = exit_signal_tx.take() {
|
||||||
let _ = tx.send(());
|
let _ = tx.send(ExecutorExitResult::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel session work
|
// Cancel session work
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ use crate::{
|
|||||||
approvals::ExecutorApprovalService,
|
approvals::ExecutorApprovalService,
|
||||||
command::{CmdOverrides, CommandBuilder, CommandParts, apply_overrides},
|
command::{CmdOverrides, CommandBuilder, CommandParts, apply_overrides},
|
||||||
executors::{
|
executors::{
|
||||||
AppendPrompt, AvailabilityInfo, ExecutorError, SpawnedChild, StandardCodingAgentExecutor,
|
AppendPrompt, AvailabilityInfo, ExecutorError, ExecutorExitResult, SpawnedChild,
|
||||||
|
StandardCodingAgentExecutor,
|
||||||
codex::{jsonrpc::ExitSignalSender, normalize_logs::Error},
|
codex::{jsonrpc::ExitSignalSender, normalize_logs::Error},
|
||||||
},
|
},
|
||||||
stdout_dup::create_stdout_pipe_writer,
|
stdout_dup::create_stdout_pipe_writer,
|
||||||
@@ -195,9 +196,13 @@ impl StandardCodingAgentExecutor for Codex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Codex {
|
impl Codex {
|
||||||
fn build_command_builder(&self) -> CommandBuilder {
|
pub fn base_command() -> &'static str {
|
||||||
let mut builder = CommandBuilder::new("npx -y @openai/codex@0.60.1 app-server");
|
"npx -y @openai/codex@0.60.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_command_builder(&self) -> CommandBuilder {
|
||||||
|
let mut builder = CommandBuilder::new(Self::base_command());
|
||||||
|
builder = builder.extend_params(["app-server"]);
|
||||||
if self.oss.unwrap_or(false) {
|
if self.oss.unwrap_or(false) {
|
||||||
builder = builder.extend_params(["--oss"]);
|
builder = builder.extend_params(["--oss"]);
|
||||||
}
|
}
|
||||||
@@ -328,17 +333,36 @@ impl Codex {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
if matches!(&err, ExecutorError::Io(io_err) if io_err.kind() == std::io::ErrorKind::BrokenPipe)
|
match &err {
|
||||||
|
ExecutorError::Io(io_err)
|
||||||
|
if io_err.kind() == std::io::ErrorKind::BrokenPipe =>
|
||||||
{
|
{
|
||||||
// Broken pipe likely means the parent process exited, so we can ignore it
|
// Broken pipe likely means the parent process exited, so we can ignore it
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ExecutorError::AuthRequired(message) => {
|
||||||
|
log_writer
|
||||||
|
.log_raw(&Error::auth_required(message.clone()).raw())
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
// Send failure signal so the process is marked as failed
|
||||||
|
exit_signal_tx
|
||||||
|
.send_exit_signal(ExecutorExitResult::Failure)
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
tracing::error!("Codex spawn error: {}", err);
|
tracing::error!("Codex spawn error: {}", err);
|
||||||
log_writer
|
log_writer
|
||||||
.log_raw(&Error::launch_error(err.to_string()).raw())
|
.log_raw(&Error::launch_error(err.to_string()).raw())
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
exit_signal_tx.send_exit_signal().await;
|
}
|
||||||
|
}
|
||||||
|
// For other errors, also send failure signal
|
||||||
|
exit_signal_tx
|
||||||
|
.send_exit_signal(ExecutorExitResult::Failure)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -365,6 +389,12 @@ impl Codex {
|
|||||||
JsonRpcPeer::spawn(child_stdin, child_stdout, client.clone(), exit_signal_tx);
|
JsonRpcPeer::spawn(child_stdin, child_stdout, client.clone(), exit_signal_tx);
|
||||||
client.connect(rpc_peer);
|
client.connect(rpc_peer);
|
||||||
client.initialize().await?;
|
client.initialize().await?;
|
||||||
|
let auth_status = client.get_auth_status().await?;
|
||||||
|
if auth_status.auth_method.is_none() {
|
||||||
|
return Err(ExecutorError::AuthRequired(
|
||||||
|
"Codex authentication required".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
match resume_session {
|
match resume_session {
|
||||||
None => {
|
None => {
|
||||||
let params = conversation_params;
|
let params = conversation_params;
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ use std::{
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use codex_app_server_protocol::{
|
use codex_app_server_protocol::{
|
||||||
AddConversationListenerParams, AddConversationSubscriptionResponse, ApplyPatchApprovalResponse,
|
AddConversationListenerParams, AddConversationSubscriptionResponse, ApplyPatchApprovalResponse,
|
||||||
ClientInfo, ClientNotification, ClientRequest, ExecCommandApprovalResponse, InitializeParams,
|
ClientInfo, ClientNotification, ClientRequest, ExecCommandApprovalResponse,
|
||||||
InitializeResponse, InputItem, JSONRPCError, JSONRPCNotification, JSONRPCRequest,
|
GetAuthStatusParams, GetAuthStatusResponse, InitializeParams, InitializeResponse, InputItem,
|
||||||
JSONRPCResponse, NewConversationParams, NewConversationResponse, RequestId,
|
JSONRPCError, JSONRPCNotification, JSONRPCRequest, JSONRPCResponse, NewConversationParams,
|
||||||
ResumeConversationParams, ResumeConversationResponse, SendUserMessageParams,
|
NewConversationResponse, RequestId, ResumeConversationParams, ResumeConversationResponse,
|
||||||
SendUserMessageResponse, ServerNotification, ServerRequest,
|
SendUserMessageParams, SendUserMessageResponse, ServerNotification, ServerRequest,
|
||||||
};
|
};
|
||||||
use codex_protocol::{ConversationId, protocol::ReviewDecision};
|
use codex_protocol::{ConversationId, protocol::ReviewDecision};
|
||||||
use serde::{Serialize, de::DeserializeOwned};
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
@@ -131,6 +131,16 @@ impl AppServerClient {
|
|||||||
self.send_request(request, "sendUserMessage").await
|
self.send_request(request, "sendUserMessage").await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_auth_status(&self) -> Result<GetAuthStatusResponse, ExecutorError> {
|
||||||
|
let request = ClientRequest::GetAuthStatus {
|
||||||
|
request_id: self.next_request_id(),
|
||||||
|
params: GetAuthStatusParams {
|
||||||
|
include_token: Some(true),
|
||||||
|
refresh_token: Some(false),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
self.send_request(request, "getAuthStatus").await
|
||||||
|
}
|
||||||
async fn handle_server_request(
|
async fn handle_server_request(
|
||||||
&self,
|
&self,
|
||||||
peer: &JsonRpcPeer,
|
peer: &JsonRpcPeer,
|
||||||
@@ -454,6 +464,7 @@ fn request_id(request: &ClientRequest) -> RequestId {
|
|||||||
match request {
|
match request {
|
||||||
ClientRequest::Initialize { request_id, .. }
|
ClientRequest::Initialize { request_id, .. }
|
||||||
| ClientRequest::NewConversation { request_id, .. }
|
| ClientRequest::NewConversation { request_id, .. }
|
||||||
|
| ClientRequest::GetAuthStatus { request_id, .. }
|
||||||
| ClientRequest::ResumeConversation { request_id, .. }
|
| ClientRequest::ResumeConversation { request_id, .. }
|
||||||
| ClientRequest::AddConversationListener { request_id, .. }
|
| ClientRequest::AddConversationListener { request_id, .. }
|
||||||
| ClientRequest::SendUserMessage { request_id, .. } => request_id.clone(),
|
| ClientRequest::SendUserMessage { request_id, .. } => request_id.clone(),
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ use tokio::{
|
|||||||
sync::{Mutex, oneshot},
|
sync::{Mutex, oneshot},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::executors::ExecutorError;
|
use crate::executors::{ExecutorError, ExecutorExitResult};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PendingResponse {
|
pub enum PendingResponse {
|
||||||
@@ -38,18 +38,19 @@ pub enum PendingResponse {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ExitSignalSender {
|
pub struct ExitSignalSender {
|
||||||
inner: Arc<Mutex<Option<oneshot::Sender<()>>>>,
|
inner: Arc<Mutex<Option<oneshot::Sender<ExecutorExitResult>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExitSignalSender {
|
impl ExitSignalSender {
|
||||||
pub fn new(sender: oneshot::Sender<()>) -> Self {
|
pub fn new(sender: oneshot::Sender<ExecutorExitResult>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: Arc::new(Mutex::new(Some(sender))),
|
inner: Arc::new(Mutex::new(Some(sender))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub async fn send_exit_signal(&self) {
|
|
||||||
|
pub async fn send_exit_signal(&self, result: ExecutorExitResult) {
|
||||||
if let Some(sender) = self.inner.lock().await.take() {
|
if let Some(sender) = self.inner.lock().await.take() {
|
||||||
let _ = sender.send(());
|
let _ = sender.send(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,7 +156,7 @@ impl JsonRpcPeer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exit_tx.send_exit_signal().await;
|
exit_tx.send_exit_signal(ExecutorExitResult::Success).await;
|
||||||
let _ = reader_peer.shutdown().await;
|
let _ = reader_peer.shutdown().await;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1035,12 +1035,16 @@ lazy_static! {
|
|||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
LaunchError { error: String },
|
LaunchError { error: String },
|
||||||
|
AuthRequired { error: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
pub fn launch_error(error: String) -> Self {
|
pub fn launch_error(error: String) -> Self {
|
||||||
Self::LaunchError { error }
|
Self::LaunchError { error }
|
||||||
}
|
}
|
||||||
|
pub fn auth_required(error: String) -> Self {
|
||||||
|
Self::AuthRequired { error }
|
||||||
|
}
|
||||||
|
|
||||||
pub fn raw(&self) -> String {
|
pub fn raw(&self) -> String {
|
||||||
serde_json::to_string(self).unwrap_or_default()
|
serde_json::to_string(self).unwrap_or_default()
|
||||||
@@ -1049,15 +1053,23 @@ impl Error {
|
|||||||
|
|
||||||
impl ToNormalizedEntry for Error {
|
impl ToNormalizedEntry for Error {
|
||||||
fn to_normalized_entry(&self) -> NormalizedEntry {
|
fn to_normalized_entry(&self) -> NormalizedEntry {
|
||||||
NormalizedEntry {
|
match self {
|
||||||
|
Error::LaunchError { error } => NormalizedEntry {
|
||||||
timestamp: None,
|
timestamp: None,
|
||||||
entry_type: NormalizedEntryType::ErrorMessage {
|
entry_type: NormalizedEntryType::ErrorMessage {
|
||||||
error_type: NormalizedEntryError::Other,
|
error_type: NormalizedEntryError::Other,
|
||||||
},
|
},
|
||||||
content: match self {
|
content: error.clone(),
|
||||||
Error::LaunchError { error } => error.clone(),
|
|
||||||
},
|
|
||||||
metadata: None,
|
metadata: None,
|
||||||
|
},
|
||||||
|
Error::AuthRequired { error } => NormalizedEntry {
|
||||||
|
timestamp: None,
|
||||||
|
entry_type: NormalizedEntryType::ErrorMessage {
|
||||||
|
error_type: NormalizedEntryError::SetupRequired,
|
||||||
|
},
|
||||||
|
content: error.clone(),
|
||||||
|
metadata: None,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ pub enum ExecutorError {
|
|||||||
ExecutableNotFound { program: String },
|
ExecutableNotFound { program: String },
|
||||||
#[error("Setup helper not supported")]
|
#[error("Setup helper not supported")]
|
||||||
SetupHelperNotSupported,
|
SetupHelperNotSupported,
|
||||||
|
#[error("Auth required: {0}")]
|
||||||
|
AuthRequired(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[enum_dispatch]
|
#[enum_dispatch]
|
||||||
@@ -154,10 +156,13 @@ impl CodingAgent {
|
|||||||
match self {
|
match self {
|
||||||
Self::ClaudeCode(_)
|
Self::ClaudeCode(_)
|
||||||
| Self::Amp(_)
|
| Self::Amp(_)
|
||||||
| Self::Codex(_)
|
|
||||||
| Self::Gemini(_)
|
| Self::Gemini(_)
|
||||||
| Self::QwenCode(_)
|
| Self::QwenCode(_)
|
||||||
| Self::Droid(_) => vec![BaseAgentCapability::SessionFork],
|
| Self::Droid(_) => vec![BaseAgentCapability::SessionFork],
|
||||||
|
Self::Codex(_) => vec![
|
||||||
|
BaseAgentCapability::SessionFork,
|
||||||
|
BaseAgentCapability::SetupHelper,
|
||||||
|
],
|
||||||
Self::CursorAgent(_) => vec![BaseAgentCapability::SetupHelper],
|
Self::CursorAgent(_) => vec![BaseAgentCapability::SetupHelper],
|
||||||
Self::Opencode(_) | Self::Copilot(_) => vec![],
|
Self::Opencode(_) | Self::Copilot(_) => vec![],
|
||||||
}
|
}
|
||||||
@@ -217,10 +222,19 @@ pub trait StandardCodingAgentExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Result communicated through the exit signal
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum ExecutorExitResult {
|
||||||
|
/// Process completed successfully (exit code 0)
|
||||||
|
Success,
|
||||||
|
/// Process should be marked as failed (non-zero exit)
|
||||||
|
Failure,
|
||||||
|
}
|
||||||
|
|
||||||
/// Optional exit notification from an executor.
|
/// Optional exit notification from an executor.
|
||||||
/// When this receiver resolves, the container should gracefully stop the process
|
/// When this receiver resolves, the container should gracefully stop the process
|
||||||
/// and mark it as successful (exit code 0).
|
/// and mark it according to the result.
|
||||||
pub type ExecutorExitSignal = tokio::sync::oneshot::Receiver<()>;
|
pub type ExecutorExitSignal = tokio::sync::oneshot::Receiver<ExecutorExitResult>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SpawnedChild {
|
pub struct SpawnedChild {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ use deployment::{DeploymentError, RemoteClientNotConfigured};
|
|||||||
use executors::{
|
use executors::{
|
||||||
actions::{Executable, ExecutorAction},
|
actions::{Executable, ExecutorAction},
|
||||||
approvals::{ExecutorApprovalService, NoopExecutorApprovalService},
|
approvals::{ExecutorApprovalService, NoopExecutorApprovalService},
|
||||||
executors::BaseCodingAgent,
|
executors::{BaseCodingAgent, ExecutorExitResult, ExecutorExitSignal},
|
||||||
logs::{
|
logs::{
|
||||||
NormalizedEntryType,
|
NormalizedEntryType,
|
||||||
utils::{
|
utils::{
|
||||||
@@ -284,7 +284,7 @@ impl LocalContainerService {
|
|||||||
pub fn spawn_exit_monitor(
|
pub fn spawn_exit_monitor(
|
||||||
&self,
|
&self,
|
||||||
exec_id: &Uuid,
|
exec_id: &Uuid,
|
||||||
exit_signal: Option<tokio::sync::oneshot::Receiver<()>>,
|
exit_signal: Option<ExecutorExitSignal>,
|
||||||
) -> JoinHandle<()> {
|
) -> JoinHandle<()> {
|
||||||
let exec_id = *exec_id;
|
let exec_id = *exec_id;
|
||||||
let child_store = self.child_store.clone();
|
let child_store = self.child_store.clone();
|
||||||
@@ -299,25 +299,31 @@ impl LocalContainerService {
|
|||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut exit_signal_future = exit_signal
|
let mut exit_signal_future = exit_signal
|
||||||
.map(|rx| rx.map(|_| ()).boxed()) // wait for signal
|
.map(|rx| rx.boxed()) // wait for result
|
||||||
.unwrap_or_else(|| std::future::pending::<()>().boxed()); // no signal, stall forever
|
.unwrap_or_else(|| std::future::pending().boxed()); // no signal, stall forever
|
||||||
|
|
||||||
let status_result: std::io::Result<std::process::ExitStatus>;
|
let status_result: std::io::Result<std::process::ExitStatus>;
|
||||||
|
|
||||||
// Wait for process to exit, or exit signal from executor
|
// Wait for process to exit, or exit signal from executor
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
// Exit signal.
|
// Exit signal with result.
|
||||||
// Some coding agent processes do not automatically exit after processing the user request; instead the executor
|
// Some coding agent processes do not automatically exit after processing the user request; instead the executor
|
||||||
// signals when processing has finished to gracefully kill the process.
|
// signals when processing has finished to gracefully kill the process.
|
||||||
_ = &mut exit_signal_future => {
|
exit_result = &mut exit_signal_future => {
|
||||||
// Executor signaled completion: kill group and remember to force Completed(0)
|
// Executor signaled completion: kill group and use the provided result
|
||||||
if let Some(child_lock) = child_store.read().await.get(&exec_id).cloned() {
|
if let Some(child_lock) = child_store.read().await.get(&exec_id).cloned() {
|
||||||
let mut child = child_lock.write().await ;
|
let mut child = child_lock.write().await ;
|
||||||
if let Err(err) = command::kill_process_group(&mut child).await {
|
if let Err(err) = command::kill_process_group(&mut child).await {
|
||||||
tracing::error!("Failed to kill process group after exit signal: {} {}", exec_id, err);
|
tracing::error!("Failed to kill process group after exit signal: {} {}", exec_id, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
status_result = Ok(success_exit_status());
|
|
||||||
|
// Map the exit result to appropriate exit status
|
||||||
|
status_result = match exit_result {
|
||||||
|
Ok(ExecutorExitResult::Success) => Ok(success_exit_status()),
|
||||||
|
Ok(ExecutorExitResult::Failure) => Ok(failure_exit_status()),
|
||||||
|
Err(_) => Ok(success_exit_status()), // Channel closed, assume success
|
||||||
|
};
|
||||||
}
|
}
|
||||||
// Process exit
|
// Process exit
|
||||||
exit_status_result = &mut process_exit_rx => {
|
exit_status_result = &mut process_exit_rx => {
|
||||||
@@ -810,6 +816,19 @@ impl LocalContainerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn failure_exit_status() -> std::process::ExitStatus {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::process::ExitStatusExt;
|
||||||
|
ExitStatusExt::from_raw(256) // Exit code 1 (shifted by 8 bits)
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
use std::os::windows::process::ExitStatusExt;
|
||||||
|
ExitStatusExt::from_raw(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ContainerService for LocalContainerService {
|
impl ContainerService for LocalContainerService {
|
||||||
fn msg_stores(&self) -> &Arc<RwLock<HashMap<Uuid, Arc<MsgStore>>>> {
|
fn msg_stores(&self) -> &Arc<RwLock<HashMap<Uuid, Arc<MsgStore>>>> {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod codex_setup;
|
||||||
pub mod cursor_setup;
|
pub mod cursor_setup;
|
||||||
pub mod drafts;
|
pub mod drafts;
|
||||||
pub mod gh_cli_setup;
|
pub mod gh_cli_setup;
|
||||||
@@ -191,6 +192,9 @@ pub async fn run_agent_setup(
|
|||||||
CodingAgent::CursorAgent(_) => {
|
CodingAgent::CursorAgent(_) => {
|
||||||
cursor_setup::run_cursor_setup(&deployment, &task_attempt).await?;
|
cursor_setup::run_cursor_setup(&deployment, &task_attempt).await?;
|
||||||
}
|
}
|
||||||
|
CodingAgent::Codex(codex) => {
|
||||||
|
codex_setup::run_codex_setup(&deployment, &task_attempt, &codex).await?;
|
||||||
|
}
|
||||||
_ => return Err(ApiError::Executor(ExecutorError::SetupHelperNotSupported)),
|
_ => return Err(ApiError::Executor(ExecutorError::SetupHelperNotSupported)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
79
crates/server/src/routes/task_attempts/codex_setup.rs
Normal file
79
crates/server/src/routes/task_attempts/codex_setup.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
use db::models::{
|
||||||
|
execution_process::{ExecutionProcess, ExecutionProcessRunReason},
|
||||||
|
task_attempt::{TaskAttempt, TaskAttemptError},
|
||||||
|
};
|
||||||
|
use deployment::Deployment;
|
||||||
|
use executors::{actions::ExecutorAction, executors::ExecutorError};
|
||||||
|
#[cfg(unix)]
|
||||||
|
use executors::{
|
||||||
|
actions::{
|
||||||
|
ExecutorActionType,
|
||||||
|
script::{ScriptContext, ScriptRequest, ScriptRequestLanguage},
|
||||||
|
},
|
||||||
|
command::CommandBuilder,
|
||||||
|
command::apply_overrides,
|
||||||
|
executors::codex::Codex,
|
||||||
|
};
|
||||||
|
use services::services::container::ContainerService;
|
||||||
|
|
||||||
|
use crate::{error::ApiError, routes::task_attempts::ensure_worktree_path};
|
||||||
|
|
||||||
|
pub async fn run_codex_setup(
|
||||||
|
deployment: &crate::DeploymentImpl,
|
||||||
|
task_attempt: &TaskAttempt,
|
||||||
|
codex: &Codex,
|
||||||
|
) -> Result<ExecutionProcess, ApiError> {
|
||||||
|
let latest_process = ExecutionProcess::find_latest_by_task_attempt_and_run_reason(
|
||||||
|
&deployment.db().pool,
|
||||||
|
task_attempt.id,
|
||||||
|
&ExecutionProcessRunReason::CodingAgent,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let executor_action = if let Some(latest_process) = latest_process {
|
||||||
|
let latest_action = latest_process
|
||||||
|
.executor_action()
|
||||||
|
.map_err(|e| ApiError::TaskAttempt(TaskAttemptError::ValidationError(e.to_string())))?;
|
||||||
|
get_setup_helper_action(codex)
|
||||||
|
.await?
|
||||||
|
.append_action(latest_action.to_owned())
|
||||||
|
} else {
|
||||||
|
get_setup_helper_action(codex).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = ensure_worktree_path(deployment, task_attempt).await?;
|
||||||
|
|
||||||
|
let execution_process = deployment
|
||||||
|
.container()
|
||||||
|
.start_execution(
|
||||||
|
task_attempt,
|
||||||
|
&executor_action,
|
||||||
|
&ExecutionProcessRunReason::SetupScript,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(execution_process)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_setup_helper_action(codex: &Codex) -> Result<ExecutorAction, ApiError> {
|
||||||
|
let mut login_command = CommandBuilder::new(Codex::base_command());
|
||||||
|
login_command = login_command.extend_params(["login"]);
|
||||||
|
login_command = apply_overrides(login_command, &codex.cmd);
|
||||||
|
|
||||||
|
let (program_path, args) = login_command
|
||||||
|
.build_initial()
|
||||||
|
.map_err(|err| ApiError::Executor(ExecutorError::from(err)))?
|
||||||
|
.into_resolved()
|
||||||
|
.await
|
||||||
|
.map_err(ApiError::Executor)?;
|
||||||
|
let login_script = format!("{} {}", program_path.to_string_lossy(), args.join(" "));
|
||||||
|
let login_request = ScriptRequest {
|
||||||
|
script: login_script,
|
||||||
|
language: ScriptRequestLanguage::Bash,
|
||||||
|
context: ScriptContext::ToolInstallScript,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ExecutorAction::new(
|
||||||
|
ExecutorActionType::ScriptRequest(login_request),
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -85,7 +85,7 @@ fi"#
|
|||||||
let install_request = ScriptRequest {
|
let install_request = ScriptRequest {
|
||||||
script: install_script,
|
script: install_script,
|
||||||
language: ScriptRequestLanguage::Bash,
|
language: ScriptRequestLanguage::Bash,
|
||||||
context: ScriptContext::SetupScript,
|
context: ScriptContext::ToolInstallScript,
|
||||||
};
|
};
|
||||||
// Second action (chained): Login
|
// Second action (chained): Login
|
||||||
let login_script = format!(
|
let login_script = format!(
|
||||||
@@ -98,7 +98,7 @@ export PATH="$HOME/.local/bin:$PATH"
|
|||||||
let login_request = ScriptRequest {
|
let login_request = ScriptRequest {
|
||||||
script: login_script,
|
script: login_script,
|
||||||
language: ScriptRequestLanguage::Bash,
|
language: ScriptRequestLanguage::Bash,
|
||||||
context: ScriptContext::SetupScript,
|
context: ScriptContext::ToolInstallScript,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Chain them: install → login
|
// Chain them: install → login
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ fi"#
|
|||||||
let install_request = ScriptRequest {
|
let install_request = ScriptRequest {
|
||||||
script: install_script,
|
script: install_script,
|
||||||
language: ScriptRequestLanguage::Bash,
|
language: ScriptRequestLanguage::Bash,
|
||||||
context: ScriptContext::GithubCliSetupScript,
|
context: ScriptContext::ToolInstallScript,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auth script
|
// Auth script
|
||||||
@@ -85,7 +85,7 @@ gh auth login --web --git-protocol https --skip-ssh-key
|
|||||||
let auth_request = ScriptRequest {
|
let auth_request = ScriptRequest {
|
||||||
script: auth_script,
|
script: auth_script,
|
||||||
language: ScriptRequestLanguage::Bash,
|
language: ScriptRequestLanguage::Bash,
|
||||||
context: ScriptContext::GithubCliSetupScript,
|
context: ScriptContext::ToolInstallScript,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Chain them: install → auth
|
// Chain them: install → auth
|
||||||
|
|||||||
@@ -125,6 +125,14 @@ pub trait ContainerService {
|
|||||||
/// - The next action is None (no follow-up actions)
|
/// - The next action is None (no follow-up actions)
|
||||||
/// - The run reason is not DevServer
|
/// - The run reason is not DevServer
|
||||||
fn should_finalize(&self, ctx: &ExecutionContext) -> bool {
|
fn should_finalize(&self, ctx: &ExecutionContext) -> bool {
|
||||||
|
// Always finalize failed or killed executions
|
||||||
|
if matches!(
|
||||||
|
ctx.execution_process.status,
|
||||||
|
ExecutionProcessStatus::Failed | ExecutionProcessStatus::Killed
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Otherwise, finalize only if no next action and not a dev server
|
||||||
ctx.execution_process
|
ctx.execution_process
|
||||||
.executor_action()
|
.executor_action()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
@@ -434,7 +434,7 @@ const ToolCallCard: React.FC<{
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// Compute defaults from entry
|
// Compute defaults from entry
|
||||||
const linkifyUrls = entryType?.tool_name === 'GitHub CLI Setup Script';
|
const linkifyUrls = entryType?.tool_name === 'Tool Install Script';
|
||||||
const defaultExpanded = linkifyUrls;
|
const defaultExpanded = linkifyUrls;
|
||||||
|
|
||||||
const [expanded, toggle] = useExpandable(
|
const [expanded, toggle] = useExpandable(
|
||||||
|
|||||||
@@ -326,8 +326,8 @@ export const useConversationHistory = ({
|
|||||||
case 'CleanupScript':
|
case 'CleanupScript':
|
||||||
toolName = 'Cleanup Script';
|
toolName = 'Cleanup Script';
|
||||||
break;
|
break;
|
||||||
case 'GithubCliSetupScript':
|
case 'ToolInstallScript':
|
||||||
toolName = 'GitHub CLI Setup Script';
|
toolName = 'Tool Install Script';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export type McpConfig = { servers: { [key in string]?: JsonValue }, servers_path
|
|||||||
|
|
||||||
export type ExecutorActionType = { "type": "CodingAgentInitialRequest" } & CodingAgentInitialRequest | { "type": "CodingAgentFollowUpRequest" } & CodingAgentFollowUpRequest | { "type": "ScriptRequest" } & ScriptRequest;
|
export type ExecutorActionType = { "type": "CodingAgentInitialRequest" } & CodingAgentInitialRequest | { "type": "CodingAgentFollowUpRequest" } & CodingAgentFollowUpRequest | { "type": "ScriptRequest" } & ScriptRequest;
|
||||||
|
|
||||||
export type ScriptContext = "SetupScript" | "CleanupScript" | "DevServer" | "GithubCliSetupScript";
|
export type ScriptContext = "SetupScript" | "CleanupScript" | "DevServer" | "ToolInstallScript";
|
||||||
|
|
||||||
export type ScriptRequest = { script: string, language: ScriptRequestLanguage, context: ScriptContext, };
|
export type ScriptRequest = { script: string, language: ScriptRequestLanguage, context: ScriptContext, };
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user