chore: clean handle_container_exec_with_params (#5516)
Drop `handle_container_exec_with_params` to have simpler and more straight forward execution path
This commit is contained in:
@@ -2316,7 +2316,11 @@ mod tests {
|
|||||||
use crate::tools::MODEL_FORMAT_MAX_LINES;
|
use crate::tools::MODEL_FORMAT_MAX_LINES;
|
||||||
use crate::tools::MODEL_FORMAT_TAIL_LINES;
|
use crate::tools::MODEL_FORMAT_TAIL_LINES;
|
||||||
use crate::tools::ToolRouter;
|
use crate::tools::ToolRouter;
|
||||||
use crate::tools::handle_container_exec_with_params;
|
use crate::tools::context::ToolInvocation;
|
||||||
|
use crate::tools::context::ToolOutput;
|
||||||
|
use crate::tools::context::ToolPayload;
|
||||||
|
use crate::tools::handlers::ShellHandler;
|
||||||
|
use crate::tools::registry::ToolHandler;
|
||||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||||
use codex_app_server_protocol::AuthMode;
|
use codex_app_server_protocol::AuthMode;
|
||||||
use codex_protocol::models::ContentItem;
|
use codex_protocol::models::ContentItem;
|
||||||
@@ -3039,15 +3043,26 @@ mod tests {
|
|||||||
let tool_name = "shell";
|
let tool_name = "shell";
|
||||||
let call_id = "test-call".to_string();
|
let call_id = "test-call".to_string();
|
||||||
|
|
||||||
let resp = handle_container_exec_with_params(
|
let handler = ShellHandler;
|
||||||
tool_name,
|
let resp = handler
|
||||||
params,
|
.handle(ToolInvocation {
|
||||||
Arc::clone(&session),
|
session: Arc::clone(&session),
|
||||||
Arc::clone(&turn_context),
|
turn: Arc::clone(&turn_context),
|
||||||
Arc::clone(&turn_diff_tracker),
|
tracker: Arc::clone(&turn_diff_tracker),
|
||||||
call_id,
|
call_id,
|
||||||
)
|
tool_name: tool_name.to_string(),
|
||||||
.await;
|
payload: ToolPayload::Function {
|
||||||
|
arguments: serde_json::json!({
|
||||||
|
"command": params.command.clone(),
|
||||||
|
"workdir": Some(turn_context.cwd.to_string_lossy().to_string()),
|
||||||
|
"timeout_ms": params.timeout_ms,
|
||||||
|
"with_escalated_permissions": params.with_escalated_permissions,
|
||||||
|
"justification": params.justification.clone(),
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
let Err(FunctionCallError::RespondToModel(output)) = resp else {
|
let Err(FunctionCallError::RespondToModel(output)) = resp else {
|
||||||
panic!("expected error result");
|
panic!("expected error result");
|
||||||
@@ -3066,17 +3081,30 @@ mod tests {
|
|||||||
.expect("unique turn context Arc")
|
.expect("unique turn context Arc")
|
||||||
.sandbox_policy = SandboxPolicy::DangerFullAccess;
|
.sandbox_policy = SandboxPolicy::DangerFullAccess;
|
||||||
|
|
||||||
let resp2 = handle_container_exec_with_params(
|
let resp2 = handler
|
||||||
tool_name,
|
.handle(ToolInvocation {
|
||||||
params2,
|
session: Arc::clone(&session),
|
||||||
Arc::clone(&session),
|
turn: Arc::clone(&turn_context),
|
||||||
Arc::clone(&turn_context),
|
tracker: Arc::clone(&turn_diff_tracker),
|
||||||
Arc::clone(&turn_diff_tracker),
|
call_id: "test-call-2".to_string(),
|
||||||
"test-call-2".to_string(),
|
tool_name: tool_name.to_string(),
|
||||||
)
|
payload: ToolPayload::Function {
|
||||||
.await;
|
arguments: serde_json::json!({
|
||||||
|
"command": params2.command.clone(),
|
||||||
|
"workdir": Some(turn_context.cwd.to_string_lossy().to_string()),
|
||||||
|
"timeout_ms": params2.timeout_ms,
|
||||||
|
"with_escalated_permissions": params2.with_escalated_permissions,
|
||||||
|
"justification": params2.justification.clone(),
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
let output = resp2.expect("expected Ok result");
|
let output = match resp2.expect("expected Ok result") {
|
||||||
|
ToolOutput::Function { content, .. } => content,
|
||||||
|
_ => panic!("unexpected tool output"),
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Deserialize, PartialEq, Eq, Debug)]
|
#[derive(Deserialize, PartialEq, Eq, Debug)]
|
||||||
struct ResponseExecMetadata {
|
struct ResponseExecMetadata {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use crate::codex::Session;
|
use crate::codex::Session;
|
||||||
use crate::codex::TurnContext;
|
use crate::codex::TurnContext;
|
||||||
|
use crate::error::CodexErr;
|
||||||
|
use crate::error::SandboxErr;
|
||||||
use crate::exec::ExecToolCallOutput;
|
use crate::exec::ExecToolCallOutput;
|
||||||
|
use crate::function_tool::FunctionCallError;
|
||||||
use crate::parse_command::parse_command;
|
use crate::parse_command::parse_command;
|
||||||
use crate::protocol::EventMsg;
|
use crate::protocol::EventMsg;
|
||||||
use crate::protocol::ExecCommandBeginEvent;
|
use crate::protocol::ExecCommandBeginEvent;
|
||||||
@@ -10,6 +13,7 @@ use crate::protocol::PatchApplyBeginEvent;
|
|||||||
use crate::protocol::PatchApplyEndEvent;
|
use crate::protocol::PatchApplyEndEvent;
|
||||||
use crate::protocol::TurnDiffEvent;
|
use crate::protocol::TurnDiffEvent;
|
||||||
use crate::tools::context::SharedTurnDiffTracker;
|
use crate::tools::context::SharedTurnDiffTracker;
|
||||||
|
use crate::tools::sandboxing::ToolError;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -202,6 +206,56 @@ impl ToolEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn begin(&self, ctx: ToolEventCtx<'_>) {
|
||||||
|
self.emit(ctx, ToolEventStage::Begin).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn finish(
|
||||||
|
&self,
|
||||||
|
ctx: ToolEventCtx<'_>,
|
||||||
|
out: Result<ExecToolCallOutput, ToolError>,
|
||||||
|
) -> Result<String, FunctionCallError> {
|
||||||
|
let event;
|
||||||
|
let result = match out {
|
||||||
|
Ok(output) => {
|
||||||
|
let content = super::format_exec_output_for_model(&output);
|
||||||
|
let exit_code = output.exit_code;
|
||||||
|
event = ToolEventStage::Success(output);
|
||||||
|
if exit_code == 0 {
|
||||||
|
Ok(content)
|
||||||
|
} else {
|
||||||
|
Err(FunctionCallError::RespondToModel(content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Timeout { output })))
|
||||||
|
| Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied { output }))) => {
|
||||||
|
let response = super::format_exec_output_for_model(&output);
|
||||||
|
event = ToolEventStage::Failure(ToolEventFailure::Output(*output));
|
||||||
|
Err(FunctionCallError::RespondToModel(response))
|
||||||
|
}
|
||||||
|
Err(ToolError::Codex(err)) => {
|
||||||
|
let message = format!("execution error: {err:?}");
|
||||||
|
let response = super::format_exec_output(&message);
|
||||||
|
event = ToolEventStage::Failure(ToolEventFailure::Message(message));
|
||||||
|
Err(FunctionCallError::RespondToModel(response))
|
||||||
|
}
|
||||||
|
Err(ToolError::Rejected(msg)) | Err(ToolError::SandboxDenied(msg)) => {
|
||||||
|
// Normalize common rejection messages for exec tools so tests and
|
||||||
|
// users see a clear, consistent phrase.
|
||||||
|
let normalized = if msg == "rejected by user" {
|
||||||
|
"exec command rejected by user".to_string()
|
||||||
|
} else {
|
||||||
|
msg
|
||||||
|
};
|
||||||
|
let response = super::format_exec_output(&normalized);
|
||||||
|
event = ToolEventStage::Failure(ToolEventFailure::Message(normalized));
|
||||||
|
Err(FunctionCallError::RespondToModel(response))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.emit(ctx, event).await;
|
||||||
|
result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn emit_exec_end(
|
async fn emit_exec_end(
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
|
use crate::apply_patch;
|
||||||
|
use crate::apply_patch::InternalApplyPatchInvocation;
|
||||||
|
use crate::apply_patch::convert_apply_patch_to_protocol;
|
||||||
use crate::client_common::tools::FreeformTool;
|
use crate::client_common::tools::FreeformTool;
|
||||||
use crate::client_common::tools::FreeformToolFormat;
|
use crate::client_common::tools::FreeformToolFormat;
|
||||||
use crate::client_common::tools::ResponsesApiTool;
|
use crate::client_common::tools::ResponsesApiTool;
|
||||||
use crate::client_common::tools::ToolSpec;
|
use crate::client_common::tools::ToolSpec;
|
||||||
use crate::exec::ExecParams;
|
|
||||||
use crate::function_tool::FunctionCallError;
|
use crate::function_tool::FunctionCallError;
|
||||||
use crate::tools::context::ToolInvocation;
|
use crate::tools::context::ToolInvocation;
|
||||||
use crate::tools::context::ToolOutput;
|
use crate::tools::context::ToolOutput;
|
||||||
use crate::tools::context::ToolPayload;
|
use crate::tools::context::ToolPayload;
|
||||||
use crate::tools::handle_container_exec_with_params;
|
use crate::tools::events::ToolEmitter;
|
||||||
|
use crate::tools::events::ToolEventCtx;
|
||||||
|
use crate::tools::orchestrator::ToolOrchestrator;
|
||||||
use crate::tools::registry::ToolHandler;
|
use crate::tools::registry::ToolHandler;
|
||||||
use crate::tools::registry::ToolKind;
|
use crate::tools::registry::ToolKind;
|
||||||
|
use crate::tools::runtimes::apply_patch::ApplyPatchRequest;
|
||||||
|
use crate::tools::runtimes::apply_patch::ApplyPatchRuntime;
|
||||||
|
use crate::tools::sandboxing::ToolCtx;
|
||||||
use crate::tools::spec::ApplyPatchToolArgs;
|
use crate::tools::spec::ApplyPatchToolArgs;
|
||||||
use crate::tools::spec::JsonSchema;
|
use crate::tools::spec::JsonSchema;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -64,30 +69,85 @@ impl ToolHandler for ApplyPatchHandler {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let exec_params = ExecParams {
|
// Re-parse and verify the patch so we can compute changes and approval.
|
||||||
command: vec!["apply_patch".to_string(), patch_input.clone()],
|
// Avoid building temporary ExecParams/command vectors; derive directly from inputs.
|
||||||
cwd: turn.cwd.clone(),
|
let cwd = turn.cwd.clone();
|
||||||
timeout_ms: None,
|
let command = vec!["apply_patch".to_string(), patch_input.clone()];
|
||||||
env: HashMap::new(),
|
match codex_apply_patch::maybe_parse_apply_patch_verified(&command, &cwd) {
|
||||||
with_escalated_permissions: None,
|
codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {
|
||||||
justification: None,
|
match apply_patch::apply_patch(session.as_ref(), turn.as_ref(), &call_id, changes)
|
||||||
arg0: None,
|
.await
|
||||||
};
|
{
|
||||||
|
InternalApplyPatchInvocation::Output(item) => {
|
||||||
|
let content = item?;
|
||||||
|
Ok(ToolOutput::Function {
|
||||||
|
content,
|
||||||
|
success: Some(true),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
InternalApplyPatchInvocation::DelegateToExec(apply) => {
|
||||||
|
let emitter = ToolEmitter::apply_patch(
|
||||||
|
convert_apply_patch_to_protocol(&apply.action),
|
||||||
|
!apply.user_explicitly_approved_this_action,
|
||||||
|
);
|
||||||
|
let event_ctx = ToolEventCtx::new(
|
||||||
|
session.as_ref(),
|
||||||
|
turn.as_ref(),
|
||||||
|
&call_id,
|
||||||
|
Some(&tracker),
|
||||||
|
);
|
||||||
|
emitter.begin(event_ctx).await;
|
||||||
|
|
||||||
let content = handle_container_exec_with_params(
|
let req = ApplyPatchRequest {
|
||||||
tool_name.as_str(),
|
patch: apply.action.patch.clone(),
|
||||||
exec_params,
|
cwd,
|
||||||
Arc::clone(&session),
|
timeout_ms: None,
|
||||||
Arc::clone(&turn),
|
user_explicitly_approved: apply.user_explicitly_approved_this_action,
|
||||||
Arc::clone(&tracker),
|
codex_exe: turn.codex_linux_sandbox_exe.clone(),
|
||||||
call_id.clone(),
|
};
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(ToolOutput::Function {
|
let mut orchestrator = ToolOrchestrator::new();
|
||||||
content,
|
let mut runtime = ApplyPatchRuntime::new();
|
||||||
success: Some(true),
|
let tool_ctx = ToolCtx {
|
||||||
})
|
session: session.as_ref(),
|
||||||
|
turn: turn.as_ref(),
|
||||||
|
call_id: call_id.clone(),
|
||||||
|
tool_name: tool_name.to_string(),
|
||||||
|
};
|
||||||
|
let out = orchestrator
|
||||||
|
.run(&mut runtime, &req, &tool_ctx, &turn, turn.approval_policy)
|
||||||
|
.await;
|
||||||
|
let event_ctx = ToolEventCtx::new(
|
||||||
|
session.as_ref(),
|
||||||
|
turn.as_ref(),
|
||||||
|
&call_id,
|
||||||
|
Some(&tracker),
|
||||||
|
);
|
||||||
|
let content = emitter.finish(event_ctx, out).await?;
|
||||||
|
Ok(ToolOutput::Function {
|
||||||
|
content,
|
||||||
|
success: Some(true),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
codex_apply_patch::MaybeApplyPatchVerified::CorrectnessError(parse_error) => {
|
||||||
|
Err(FunctionCallError::RespondToModel(format!(
|
||||||
|
"apply_patch verification failed: {parse_error}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
codex_apply_patch::MaybeApplyPatchVerified::ShellParseError(error) => {
|
||||||
|
tracing::trace!("Failed to parse apply_patch input, {error:?}");
|
||||||
|
Err(FunctionCallError::RespondToModel(
|
||||||
|
"apply_patch handler received invalid patch input".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
codex_apply_patch::MaybeApplyPatchVerified::NotApplyPatch => {
|
||||||
|
Err(FunctionCallError::RespondToModel(
|
||||||
|
"apply_patch handler received non-apply_patch input".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ use async_trait::async_trait;
|
|||||||
use codex_protocol::models::ShellToolCallParams;
|
use codex_protocol::models::ShellToolCallParams;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::apply_patch;
|
||||||
|
use crate::apply_patch::InternalApplyPatchInvocation;
|
||||||
|
use crate::apply_patch::convert_apply_patch_to_protocol;
|
||||||
use crate::codex::TurnContext;
|
use crate::codex::TurnContext;
|
||||||
use crate::exec::ExecParams;
|
use crate::exec::ExecParams;
|
||||||
use crate::exec_env::create_env;
|
use crate::exec_env::create_env;
|
||||||
@@ -9,9 +12,16 @@ use crate::function_tool::FunctionCallError;
|
|||||||
use crate::tools::context::ToolInvocation;
|
use crate::tools::context::ToolInvocation;
|
||||||
use crate::tools::context::ToolOutput;
|
use crate::tools::context::ToolOutput;
|
||||||
use crate::tools::context::ToolPayload;
|
use crate::tools::context::ToolPayload;
|
||||||
use crate::tools::handle_container_exec_with_params;
|
use crate::tools::events::ToolEmitter;
|
||||||
|
use crate::tools::events::ToolEventCtx;
|
||||||
|
use crate::tools::orchestrator::ToolOrchestrator;
|
||||||
use crate::tools::registry::ToolHandler;
|
use crate::tools::registry::ToolHandler;
|
||||||
use crate::tools::registry::ToolKind;
|
use crate::tools::registry::ToolKind;
|
||||||
|
use crate::tools::runtimes::apply_patch::ApplyPatchRequest;
|
||||||
|
use crate::tools::runtimes::apply_patch::ApplyPatchRuntime;
|
||||||
|
use crate::tools::runtimes::shell::ShellRequest;
|
||||||
|
use crate::tools::runtimes::shell::ShellRuntime;
|
||||||
|
use crate::tools::sandboxing::ToolCtx;
|
||||||
|
|
||||||
pub struct ShellHandler;
|
pub struct ShellHandler;
|
||||||
|
|
||||||
@@ -61,35 +71,27 @@ impl ToolHandler for ShellHandler {
|
|||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
let exec_params = Self::to_exec_params(params, turn.as_ref());
|
let exec_params = Self::to_exec_params(params, turn.as_ref());
|
||||||
let content = handle_container_exec_with_params(
|
Self::run_exec_like(
|
||||||
tool_name.as_str(),
|
tool_name.as_str(),
|
||||||
exec_params,
|
exec_params,
|
||||||
Arc::clone(&session),
|
session,
|
||||||
Arc::clone(&turn),
|
turn,
|
||||||
Arc::clone(&tracker),
|
tracker,
|
||||||
call_id.clone(),
|
call_id,
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
Ok(ToolOutput::Function {
|
|
||||||
content,
|
|
||||||
success: Some(true),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
ToolPayload::LocalShell { params } => {
|
ToolPayload::LocalShell { params } => {
|
||||||
let exec_params = Self::to_exec_params(params, turn.as_ref());
|
let exec_params = Self::to_exec_params(params, turn.as_ref());
|
||||||
let content = handle_container_exec_with_params(
|
Self::run_exec_like(
|
||||||
tool_name.as_str(),
|
tool_name.as_str(),
|
||||||
exec_params,
|
exec_params,
|
||||||
Arc::clone(&session),
|
session,
|
||||||
Arc::clone(&turn),
|
turn,
|
||||||
Arc::clone(&tracker),
|
tracker,
|
||||||
call_id.clone(),
|
call_id,
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
Ok(ToolOutput::Function {
|
|
||||||
content,
|
|
||||||
success: Some(true),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
_ => Err(FunctionCallError::RespondToModel(format!(
|
_ => Err(FunctionCallError::RespondToModel(format!(
|
||||||
"unsupported payload for shell handler: {tool_name}"
|
"unsupported payload for shell handler: {tool_name}"
|
||||||
@@ -97,3 +99,134 @@ impl ToolHandler for ShellHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ShellHandler {
|
||||||
|
async fn run_exec_like(
|
||||||
|
tool_name: &str,
|
||||||
|
exec_params: ExecParams,
|
||||||
|
session: Arc<crate::codex::Session>,
|
||||||
|
turn: Arc<TurnContext>,
|
||||||
|
tracker: crate::tools::context::SharedTurnDiffTracker,
|
||||||
|
call_id: String,
|
||||||
|
) -> Result<ToolOutput, FunctionCallError> {
|
||||||
|
// Approval policy guard for explicit escalation in non-OnRequest modes.
|
||||||
|
if exec_params.with_escalated_permissions.unwrap_or(false)
|
||||||
|
&& !matches!(
|
||||||
|
turn.approval_policy,
|
||||||
|
codex_protocol::protocol::AskForApproval::OnRequest
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return Err(FunctionCallError::RespondToModel(format!(
|
||||||
|
"approval policy is {policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {policy:?}",
|
||||||
|
policy = turn.approval_policy
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intercept apply_patch if present.
|
||||||
|
match codex_apply_patch::maybe_parse_apply_patch_verified(
|
||||||
|
&exec_params.command,
|
||||||
|
&exec_params.cwd,
|
||||||
|
) {
|
||||||
|
codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {
|
||||||
|
match apply_patch::apply_patch(session.as_ref(), turn.as_ref(), &call_id, changes)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
InternalApplyPatchInvocation::Output(item) => {
|
||||||
|
// Programmatic apply_patch path; return its result.
|
||||||
|
let content = item?;
|
||||||
|
return Ok(ToolOutput::Function {
|
||||||
|
content,
|
||||||
|
success: Some(true),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
InternalApplyPatchInvocation::DelegateToExec(apply) => {
|
||||||
|
let emitter = ToolEmitter::apply_patch(
|
||||||
|
convert_apply_patch_to_protocol(&apply.action),
|
||||||
|
!apply.user_explicitly_approved_this_action,
|
||||||
|
);
|
||||||
|
let event_ctx = ToolEventCtx::new(
|
||||||
|
session.as_ref(),
|
||||||
|
turn.as_ref(),
|
||||||
|
&call_id,
|
||||||
|
Some(&tracker),
|
||||||
|
);
|
||||||
|
emitter.begin(event_ctx).await;
|
||||||
|
|
||||||
|
let req = ApplyPatchRequest {
|
||||||
|
patch: apply.action.patch.clone(),
|
||||||
|
cwd: exec_params.cwd.clone(),
|
||||||
|
timeout_ms: exec_params.timeout_ms,
|
||||||
|
user_explicitly_approved: apply.user_explicitly_approved_this_action,
|
||||||
|
codex_exe: turn.codex_linux_sandbox_exe.clone(),
|
||||||
|
};
|
||||||
|
let mut orchestrator = ToolOrchestrator::new();
|
||||||
|
let mut runtime = ApplyPatchRuntime::new();
|
||||||
|
let tool_ctx = ToolCtx {
|
||||||
|
session: session.as_ref(),
|
||||||
|
turn: turn.as_ref(),
|
||||||
|
call_id: call_id.clone(),
|
||||||
|
tool_name: tool_name.to_string(),
|
||||||
|
};
|
||||||
|
let out = orchestrator
|
||||||
|
.run(&mut runtime, &req, &tool_ctx, &turn, turn.approval_policy)
|
||||||
|
.await;
|
||||||
|
let event_ctx = ToolEventCtx::new(
|
||||||
|
session.as_ref(),
|
||||||
|
turn.as_ref(),
|
||||||
|
&call_id,
|
||||||
|
Some(&tracker),
|
||||||
|
);
|
||||||
|
let content = emitter.finish(event_ctx, out).await?;
|
||||||
|
return Ok(ToolOutput::Function {
|
||||||
|
content,
|
||||||
|
success: Some(true),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
codex_apply_patch::MaybeApplyPatchVerified::CorrectnessError(parse_error) => {
|
||||||
|
return Err(FunctionCallError::RespondToModel(format!(
|
||||||
|
"apply_patch verification failed: {parse_error}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
codex_apply_patch::MaybeApplyPatchVerified::ShellParseError(error) => {
|
||||||
|
tracing::trace!("Failed to parse shell command, {error:?}");
|
||||||
|
// Fall through to regular shell execution.
|
||||||
|
}
|
||||||
|
codex_apply_patch::MaybeApplyPatchVerified::NotApplyPatch => {
|
||||||
|
// Fall through to regular shell execution.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular shell execution path.
|
||||||
|
let emitter = ToolEmitter::shell(exec_params.command.clone(), exec_params.cwd.clone());
|
||||||
|
let event_ctx = ToolEventCtx::new(session.as_ref(), turn.as_ref(), &call_id, None);
|
||||||
|
emitter.begin(event_ctx).await;
|
||||||
|
|
||||||
|
let req = ShellRequest {
|
||||||
|
command: exec_params.command.clone(),
|
||||||
|
cwd: exec_params.cwd.clone(),
|
||||||
|
timeout_ms: exec_params.timeout_ms,
|
||||||
|
env: exec_params.env.clone(),
|
||||||
|
with_escalated_permissions: exec_params.with_escalated_permissions,
|
||||||
|
justification: exec_params.justification.clone(),
|
||||||
|
};
|
||||||
|
let mut orchestrator = ToolOrchestrator::new();
|
||||||
|
let mut runtime = ShellRuntime::new();
|
||||||
|
let tool_ctx = ToolCtx {
|
||||||
|
session: session.as_ref(),
|
||||||
|
turn: turn.as_ref(),
|
||||||
|
call_id: call_id.clone(),
|
||||||
|
tool_name: tool_name.to_string(),
|
||||||
|
};
|
||||||
|
let out = orchestrator
|
||||||
|
.run(&mut runtime, &req, &tool_ctx, &turn, turn.approval_policy)
|
||||||
|
.await;
|
||||||
|
let event_ctx = ToolEventCtx::new(session.as_ref(), turn.as_ref(), &call_id, None);
|
||||||
|
let content = emitter.finish(event_ctx, out).await?;
|
||||||
|
Ok(ToolOutput::Function {
|
||||||
|
content,
|
||||||
|
success: Some(true),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,37 +9,11 @@ pub mod runtimes;
|
|||||||
pub mod sandboxing;
|
pub mod sandboxing;
|
||||||
pub mod spec;
|
pub mod spec;
|
||||||
|
|
||||||
use crate::apply_patch;
|
|
||||||
use crate::apply_patch::InternalApplyPatchInvocation;
|
|
||||||
use crate::apply_patch::convert_apply_patch_to_protocol;
|
|
||||||
use crate::codex::Session;
|
|
||||||
use crate::codex::TurnContext;
|
|
||||||
use crate::error::CodexErr;
|
|
||||||
use crate::error::SandboxErr;
|
|
||||||
use crate::exec::ExecParams;
|
|
||||||
use crate::exec::ExecToolCallOutput;
|
use crate::exec::ExecToolCallOutput;
|
||||||
use crate::function_tool::FunctionCallError;
|
|
||||||
use crate::tools::context::SharedTurnDiffTracker;
|
|
||||||
use crate::tools::events::ToolEmitter;
|
|
||||||
use crate::tools::events::ToolEventCtx;
|
|
||||||
use crate::tools::events::ToolEventFailure;
|
|
||||||
use crate::tools::events::ToolEventStage;
|
|
||||||
use crate::tools::orchestrator::ToolOrchestrator;
|
|
||||||
use crate::tools::runtimes::apply_patch::ApplyPatchRequest;
|
|
||||||
use crate::tools::runtimes::apply_patch::ApplyPatchRuntime;
|
|
||||||
use crate::tools::runtimes::shell::ShellRequest;
|
|
||||||
use crate::tools::runtimes::shell::ShellRuntime;
|
|
||||||
use crate::tools::sandboxing::ToolCtx;
|
|
||||||
use crate::tools::sandboxing::ToolError;
|
|
||||||
use codex_apply_patch::MaybeApplyPatchVerified;
|
|
||||||
use codex_apply_patch::maybe_parse_apply_patch_verified;
|
|
||||||
use codex_protocol::protocol::AskForApproval;
|
|
||||||
use codex_utils_string::take_bytes_at_char_boundary;
|
use codex_utils_string::take_bytes_at_char_boundary;
|
||||||
use codex_utils_string::take_last_bytes_at_char_boundary;
|
use codex_utils_string::take_last_bytes_at_char_boundary;
|
||||||
pub use router::ToolRouter;
|
pub use router::ToolRouter;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::sync::Arc;
|
|
||||||
use tracing::trace;
|
|
||||||
|
|
||||||
// Model-formatting limits: clients get full streams; only content sent to the model is truncated.
|
// Model-formatting limits: clients get full streams; only content sent to the model is truncated.
|
||||||
pub(crate) const MODEL_FORMAT_MAX_BYTES: usize = 10 * 1024; // 10 KiB
|
pub(crate) const MODEL_FORMAT_MAX_BYTES: usize = 10 * 1024; // 10 KiB
|
||||||
@@ -54,186 +28,6 @@ pub(crate) const TELEMETRY_PREVIEW_MAX_LINES: usize = 64; // lines
|
|||||||
pub(crate) const TELEMETRY_PREVIEW_TRUNCATION_NOTICE: &str =
|
pub(crate) const TELEMETRY_PREVIEW_TRUNCATION_NOTICE: &str =
|
||||||
"[... telemetry preview truncated ...]";
|
"[... telemetry preview truncated ...]";
|
||||||
|
|
||||||
// TODO(jif) break this down
|
|
||||||
pub(crate) async fn handle_container_exec_with_params(
|
|
||||||
tool_name: &str,
|
|
||||||
params: ExecParams,
|
|
||||||
sess: Arc<Session>,
|
|
||||||
turn_context: Arc<TurnContext>,
|
|
||||||
turn_diff_tracker: SharedTurnDiffTracker,
|
|
||||||
call_id: String,
|
|
||||||
) -> Result<String, FunctionCallError> {
|
|
||||||
let _otel_event_manager = turn_context.client.get_otel_event_manager();
|
|
||||||
|
|
||||||
if params.with_escalated_permissions.unwrap_or(false)
|
|
||||||
&& !matches!(turn_context.approval_policy, AskForApproval::OnRequest)
|
|
||||||
{
|
|
||||||
return Err(FunctionCallError::RespondToModel(format!(
|
|
||||||
"approval policy is {policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {policy:?}",
|
|
||||||
policy = turn_context.approval_policy
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if this was a patch, and apply it if so
|
|
||||||
let apply_patch_exec = match maybe_parse_apply_patch_verified(¶ms.command, ¶ms.cwd) {
|
|
||||||
MaybeApplyPatchVerified::Body(changes) => {
|
|
||||||
match apply_patch::apply_patch(sess.as_ref(), turn_context.as_ref(), &call_id, changes)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
InternalApplyPatchInvocation::Output(item) => return item,
|
|
||||||
InternalApplyPatchInvocation::DelegateToExec(apply_patch_exec) => {
|
|
||||||
Some(apply_patch_exec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MaybeApplyPatchVerified::CorrectnessError(parse_error) => {
|
|
||||||
// It looks like an invocation of `apply_patch`, but we
|
|
||||||
// could not resolve it into a patch that would apply
|
|
||||||
// cleanly. Return to model for resample.
|
|
||||||
return Err(FunctionCallError::RespondToModel(format!(
|
|
||||||
"apply_patch verification failed: {parse_error}"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
MaybeApplyPatchVerified::ShellParseError(error) => {
|
|
||||||
trace!("Failed to parse shell command, {error:?}");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
MaybeApplyPatchVerified::NotApplyPatch => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let (event_emitter, diff_opt) = match apply_patch_exec.as_ref() {
|
|
||||||
Some(exec) => (
|
|
||||||
ToolEmitter::apply_patch(
|
|
||||||
convert_apply_patch_to_protocol(&exec.action),
|
|
||||||
!exec.user_explicitly_approved_this_action,
|
|
||||||
),
|
|
||||||
Some(&turn_diff_tracker),
|
|
||||||
),
|
|
||||||
None => (
|
|
||||||
ToolEmitter::shell(params.command.clone(), params.cwd.clone()),
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let event_ctx = ToolEventCtx::new(sess.as_ref(), turn_context.as_ref(), &call_id, diff_opt);
|
|
||||||
event_emitter.emit(event_ctx, ToolEventStage::Begin).await;
|
|
||||||
|
|
||||||
// Build runtime contexts only when needed (shell/apply_patch below).
|
|
||||||
|
|
||||||
if let Some(exec) = apply_patch_exec {
|
|
||||||
// Route apply_patch execution through the new orchestrator/runtime.
|
|
||||||
let req = ApplyPatchRequest {
|
|
||||||
patch: exec.action.patch.clone(),
|
|
||||||
cwd: params.cwd.clone(),
|
|
||||||
timeout_ms: params.timeout_ms,
|
|
||||||
user_explicitly_approved: exec.user_explicitly_approved_this_action,
|
|
||||||
codex_exe: turn_context.codex_linux_sandbox_exe.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut orchestrator = ToolOrchestrator::new();
|
|
||||||
let mut runtime = ApplyPatchRuntime::new();
|
|
||||||
let tool_ctx = ToolCtx {
|
|
||||||
session: sess.as_ref(),
|
|
||||||
turn: turn_context.as_ref(),
|
|
||||||
call_id: call_id.clone(),
|
|
||||||
tool_name: tool_name.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let out = orchestrator
|
|
||||||
.run(
|
|
||||||
&mut runtime,
|
|
||||||
&req,
|
|
||||||
&tool_ctx,
|
|
||||||
&turn_context,
|
|
||||||
turn_context.approval_policy,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
handle_exec_outcome(&event_emitter, event_ctx, out).await
|
|
||||||
} else {
|
|
||||||
// Route shell execution through the new orchestrator/runtime.
|
|
||||||
let req = ShellRequest {
|
|
||||||
command: params.command.clone(),
|
|
||||||
cwd: params.cwd.clone(),
|
|
||||||
timeout_ms: params.timeout_ms,
|
|
||||||
env: params.env.clone(),
|
|
||||||
with_escalated_permissions: params.with_escalated_permissions,
|
|
||||||
justification: params.justification.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut orchestrator = ToolOrchestrator::new();
|
|
||||||
let mut runtime = ShellRuntime::new();
|
|
||||||
let tool_ctx = ToolCtx {
|
|
||||||
session: sess.as_ref(),
|
|
||||||
turn: turn_context.as_ref(),
|
|
||||||
call_id: call_id.clone(),
|
|
||||||
tool_name: tool_name.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let out = orchestrator
|
|
||||||
.run(
|
|
||||||
&mut runtime,
|
|
||||||
&req,
|
|
||||||
&tool_ctx,
|
|
||||||
&turn_context,
|
|
||||||
turn_context.approval_policy,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
handle_exec_outcome(&event_emitter, event_ctx, out).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_exec_outcome(
|
|
||||||
event_emitter: &ToolEmitter,
|
|
||||||
event_ctx: ToolEventCtx<'_>,
|
|
||||||
out: Result<ExecToolCallOutput, ToolError>,
|
|
||||||
) -> Result<String, FunctionCallError> {
|
|
||||||
let event;
|
|
||||||
let result = match out {
|
|
||||||
Ok(output) => {
|
|
||||||
let content = format_exec_output_for_model(&output);
|
|
||||||
let exit_code = output.exit_code;
|
|
||||||
event = ToolEventStage::Success(output);
|
|
||||||
if exit_code == 0 {
|
|
||||||
Ok(content)
|
|
||||||
} else {
|
|
||||||
Err(FunctionCallError::RespondToModel(content))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Timeout { output })))
|
|
||||||
| Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied { output }))) => {
|
|
||||||
let response = format_exec_output_for_model(&output);
|
|
||||||
event = ToolEventStage::Failure(ToolEventFailure::Output(*output));
|
|
||||||
Err(FunctionCallError::RespondToModel(response))
|
|
||||||
}
|
|
||||||
Err(ToolError::Codex(err)) => {
|
|
||||||
let message = format!("execution error: {err:?}");
|
|
||||||
let response = format_exec_output(&message);
|
|
||||||
event = ToolEventStage::Failure(ToolEventFailure::Message(message));
|
|
||||||
Err(FunctionCallError::RespondToModel(format_exec_output(
|
|
||||||
&response,
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
Err(ToolError::Rejected(msg)) | Err(ToolError::SandboxDenied(msg)) => {
|
|
||||||
// Normalize common rejection messages for exec tools so tests and
|
|
||||||
// users see a clear, consistent phrase.
|
|
||||||
let normalized = if msg == "rejected by user" {
|
|
||||||
"exec command rejected by user".to_string()
|
|
||||||
} else {
|
|
||||||
msg
|
|
||||||
};
|
|
||||||
let response = format_exec_output(&normalized);
|
|
||||||
event = ToolEventStage::Failure(ToolEventFailure::Message(normalized));
|
|
||||||
Err(FunctionCallError::RespondToModel(format_exec_output(
|
|
||||||
&response,
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
event_emitter.emit(event_ctx, event).await;
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format the combined exec output for sending back to the model.
|
/// Format the combined exec output for sending back to the model.
|
||||||
/// Includes exit code and duration metadata; truncates large bodies safely.
|
/// Includes exit code and duration metadata; truncates large bodies safely.
|
||||||
pub fn format_exec_output_for_model(exec_output: &ExecToolCallOutput) -> String {
|
pub fn format_exec_output_for_model(exec_output: &ExecToolCallOutput) -> String {
|
||||||
@@ -363,6 +157,7 @@ fn truncate_formatted_exec_output(content: &str, total_lines: usize) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::function_tool::FunctionCallError;
|
||||||
use regex_lite::Regex;
|
use regex_lite::Regex;
|
||||||
|
|
||||||
fn truncate_function_error(err: FunctionCallError) -> FunctionCallError {
|
fn truncate_function_error(err: FunctionCallError) -> FunctionCallError {
|
||||||
|
|||||||
Reference in New Issue
Block a user