mirror of
https://github.com/openai/codex.git
synced 2026-10-21 05:22:21 +00:00
Use step environments for Guardian approval reviews (#37618)
## Why Deferred environments can become ready after a turn starts. Guardian approval reviews must use the environment selected for the current step so their working directory and permission context are not taken from the stale turn snapshot. ## What changed - Carry `StepContext` through tool execution and approval handling. - Add a Guardian review context that pairs the parent turn with the current step's environment snapshot. - Use that snapshot for Guardian prompts, reviewer sessions, and delegated reviewer threads while retaining turn-level fallbacks for callers without a step context. ## Testing Added an integration test that makes a deferred remote environment ready before an escalated command and verifies Guardian receives that environment's working directory and denied-read policy instead of the initial local environment's. GitOrigin-RevId: 779096bc7d0e0c7453a009c02141029714b9b407
This commit is contained in:
@@ -36,6 +36,7 @@ use tokio::time::timeout;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::environment_selection::TurnEnvironmentSnapshot;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::GuardianReviewOptions;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
@@ -84,6 +85,7 @@ pub(crate) async fn run_codex_thread_interactive(
|
||||
models_manager: SharedModelsManager,
|
||||
parent_session: Arc<Session>,
|
||||
parent_ctx: Arc<TurnContext>,
|
||||
parent_environments: TurnEnvironmentSnapshot,
|
||||
cancel_token: CancellationToken,
|
||||
subagent_source: SubAgentSource,
|
||||
initial_history: Option<InitialHistory>,
|
||||
@@ -126,11 +128,11 @@ pub(crate) async fn run_codex_thread_interactive(
|
||||
dynamic_tools: Vec::new(),
|
||||
metrics_service_name: None,
|
||||
user_shell_override: None,
|
||||
inherited_environments: Some(parent_ctx.environments.clone()),
|
||||
inherited_environments: Some(parent_environments.clone()),
|
||||
inherited_exec_policy: Some(Arc::clone(&parent_session.services.exec_policy)),
|
||||
parent_rollout_thread_trace: codex_rollout_trace::ThreadTraceContext::disabled(),
|
||||
parent_trace: None,
|
||||
environment_selections: parent_ctx.environments.to_selections(),
|
||||
environment_selections: parent_environments.to_selections(),
|
||||
thread_extension_init: codex_extension_api::ExtensionDataInit::default(),
|
||||
client_mcp_extensions: parent_session.services.client_mcp_extensions.clone(),
|
||||
analytics_events_client: Some(parent_session.services.analytics_events_client.clone()),
|
||||
@@ -216,12 +218,14 @@ pub(crate) async fn run_codex_thread_one_shot(
|
||||
// requiring the caller to cancel the parent token.
|
||||
let child_cancel = cancel_token.child_token();
|
||||
let parent_turn_id = parent_ctx.sub_id.clone();
|
||||
let parent_environments = parent_ctx.environments.clone();
|
||||
let (session, io) = Box::pin(run_codex_thread_interactive(
|
||||
config,
|
||||
auth_manager,
|
||||
models_manager,
|
||||
parent_session,
|
||||
parent_ctx,
|
||||
parent_environments,
|
||||
child_cancel.clone(),
|
||||
subagent_source,
|
||||
initial_history,
|
||||
|
||||
@@ -201,6 +201,7 @@ async fn run_codex_thread_interactive_respects_pre_cancelled_spawn() {
|
||||
crate::session::tests::make_session_and_context_with_rx().await;
|
||||
let cancel_token = CancellationToken::new();
|
||||
cancel_token.cancel();
|
||||
let parent_environments = parent_ctx.environments.clone();
|
||||
|
||||
let result = timeout(
|
||||
Duration::from_secs(/*secs*/ 1),
|
||||
@@ -210,6 +211,7 @@ async fn run_codex_thread_interactive_respects_pre_cancelled_spawn() {
|
||||
Arc::clone(&parent_session.services.models_manager),
|
||||
parent_session,
|
||||
parent_ctx,
|
||||
parent_environments,
|
||||
cancel_token,
|
||||
SubAgentSource::Review,
|
||||
/*initial_history*/ None,
|
||||
|
||||
@@ -17,12 +17,16 @@ mod prompt;
|
||||
mod review;
|
||||
mod review_session;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_protocol::protocol::GuardianAssessmentOutcome;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::environment_selection::TurnEnvironmentSnapshot;
|
||||
use crate::session::step_context::StepContext;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::tools::sandboxing::ApprovalRequestReasons;
|
||||
|
||||
pub(crate) use approval_request::GuardianApprovalRequest;
|
||||
@@ -62,6 +66,51 @@ const GUARDIAN_MAX_ACTION_STRING_TOKENS: usize = 16_000;
|
||||
const GUARDIAN_RECENT_ENTRY_LIMIT: usize = 40;
|
||||
const TRUNCATION_TAG: &str = "truncated";
|
||||
|
||||
/// Built from the originating StepContext when available.
|
||||
/// There are currently two exceptions-- turn-only callers (background network approvals, reviewer
|
||||
/// prewarming, etc.) and interactive Unix shells that can outlive the step that started them.
|
||||
/// TODO(sayan): See if we can find a way to model those as StepContext as well without holding
|
||||
/// step-scoped things past their lifetime (like MCP bindings)
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct GuardianReviewContext {
|
||||
turn: Arc<TurnContext>,
|
||||
environments: TurnEnvironmentSnapshot,
|
||||
}
|
||||
|
||||
impl GuardianReviewContext {
|
||||
pub(crate) fn turn(&self) -> &Arc<TurnContext> {
|
||||
&self.turn
|
||||
}
|
||||
|
||||
pub(crate) fn environments(&self) -> &TurnEnvironmentSnapshot {
|
||||
&self.environments
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Arc<StepContext>> for GuardianReviewContext {
|
||||
fn from(step: &Arc<StepContext>) -> Self {
|
||||
Self {
|
||||
turn: Arc::clone(&step.turn),
|
||||
environments: step.environments.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<TurnContext>> for GuardianReviewContext {
|
||||
fn from(turn: Arc<TurnContext>) -> Self {
|
||||
Self {
|
||||
environments: turn.environments.clone(),
|
||||
turn,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Arc<TurnContext>> for GuardianReviewContext {
|
||||
fn from(turn: &Arc<TurnContext>) -> Self {
|
||||
Self::from(Arc::clone(turn))
|
||||
}
|
||||
}
|
||||
|
||||
/// Structured output contract that the guardian reviewer must satisfy.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub(crate) struct GuardianAssessment {
|
||||
|
||||
@@ -11,7 +11,6 @@ use serde_json::Value;
|
||||
use crate::compact::content_items_to_text;
|
||||
use crate::event_mapping::is_contextual_user_message_content;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
use codex_utils_output_truncation::TruncationPolicy;
|
||||
use codex_utils_output_truncation::approx_bytes_for_tokens;
|
||||
@@ -28,6 +27,7 @@ use super::GUARDIAN_MAX_TOOL_TRANSCRIPT_TOKENS;
|
||||
use super::GUARDIAN_RECENT_ENTRY_LIMIT;
|
||||
use super::GuardianApprovalRequest;
|
||||
use super::GuardianAssessment;
|
||||
use super::GuardianReviewContext;
|
||||
use super::TRUNCATION_TAG;
|
||||
use super::approval_request::format_guardian_action_pretty;
|
||||
|
||||
@@ -103,7 +103,7 @@ pub(crate) async fn build_guardian_prompt_items(
|
||||
) -> serde_json::Result<GuardianPromptItems> {
|
||||
build_guardian_prompt_items_with_parent_turn(
|
||||
session,
|
||||
/*parent_turn*/ None,
|
||||
/*parent_context*/ None,
|
||||
ApprovalRequestReasons {
|
||||
approval: None,
|
||||
retry: retry_reason,
|
||||
@@ -116,7 +116,7 @@ pub(crate) async fn build_guardian_prompt_items(
|
||||
|
||||
pub(crate) async fn build_guardian_prompt_items_with_parent_turn(
|
||||
session: &Session,
|
||||
parent_turn: Option<&TurnContext>,
|
||||
parent_context: Option<&GuardianReviewContext>,
|
||||
reasons: ApprovalRequestReasons,
|
||||
request: GuardianApprovalRequest,
|
||||
mode: GuardianPromptMode,
|
||||
@@ -201,7 +201,7 @@ pub(crate) async fn build_guardian_prompt_items_with_parent_turn(
|
||||
if let Some(note) = omission_note {
|
||||
push_text(format!("\n{note}\n"));
|
||||
}
|
||||
if let Some(denied_reads_context) = parent_turn.and_then(parent_turn_denied_reads_context) {
|
||||
if let Some(denied_reads_context) = parent_context.and_then(parent_turn_denied_reads_context) {
|
||||
push_text("\n>>> PARENT TURN PERMISSION CONTEXT START\n".to_string());
|
||||
push_text(denied_reads_context);
|
||||
push_text(">>> PARENT TURN PERMISSION CONTEXT END\n".to_string());
|
||||
@@ -254,9 +254,9 @@ pub(crate) async fn build_guardian_prompt_items_with_parent_turn(
|
||||
})
|
||||
}
|
||||
|
||||
fn parent_turn_denied_reads_context(turn: &TurnContext) -> Option<String> {
|
||||
// TODO(sayan): Pass StepContext through Guardian so it sees environments that become ready mid-turn.
|
||||
let environment = turn.environments.primary();
|
||||
fn parent_turn_denied_reads_context(context: &GuardianReviewContext) -> Option<String> {
|
||||
let turn = context.turn();
|
||||
let environment = context.environments().primary();
|
||||
#[allow(deprecated)]
|
||||
let cwd = environment
|
||||
.and_then(|environment| environment.cwd().to_abs_path().ok())
|
||||
|
||||
@@ -41,6 +41,7 @@ use super::GuardianAssessment;
|
||||
use super::GuardianAssessmentOutcome;
|
||||
use super::GuardianRejectionCircuitBreakerAction;
|
||||
use super::GuardianRejectionCircuitBreakerPolicy;
|
||||
use super::GuardianReviewContext;
|
||||
use super::approval_request::guardian_assessment_action;
|
||||
use super::approval_request::guardian_request_target_item_id;
|
||||
use super::approval_request::guardian_request_turn_id;
|
||||
@@ -303,12 +304,13 @@ pub(crate) async fn record_guardian_denial_for_test(
|
||||
/// caller as distinct from explicit guardian denials.
|
||||
async fn run_guardian_review(
|
||||
session: Arc<Session>,
|
||||
turn: Arc<TurnContext>,
|
||||
context: GuardianReviewContext,
|
||||
review_id: String,
|
||||
request: GuardianApprovalRequest,
|
||||
reasons: ApprovalRequestReasons,
|
||||
options: GuardianReviewOptions,
|
||||
) -> ReviewDecision {
|
||||
let turn = Arc::clone(context.turn());
|
||||
let GuardianReviewOptions {
|
||||
plugin_attribution_override,
|
||||
approval_request_source,
|
||||
@@ -401,7 +403,7 @@ async fn run_guardian_review(
|
||||
let terminal_action = action_summary.clone();
|
||||
let (outcome, analytics_result) = Box::pin(run_guardian_review_session_with_retry(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
context,
|
||||
request,
|
||||
reasons,
|
||||
schema,
|
||||
@@ -643,7 +645,7 @@ pub(crate) struct GuardianReviewOptions {
|
||||
/// Public entrypoint for approval requests that should be reviewed by guardian.
|
||||
pub(crate) async fn review_approval_request(
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
context: impl Into<GuardianReviewContext>,
|
||||
review_id: String,
|
||||
request: GuardianApprovalRequest,
|
||||
reasons: ApprovalRequestReasons,
|
||||
@@ -652,7 +654,7 @@ pub(crate) async fn review_approval_request(
|
||||
// guardian session state machine into their own async stack.
|
||||
Box::pin(run_guardian_review(
|
||||
Arc::clone(session),
|
||||
Arc::clone(turn),
|
||||
context.into(),
|
||||
review_id,
|
||||
request,
|
||||
reasons,
|
||||
@@ -667,7 +669,7 @@ pub(crate) async fn review_approval_request(
|
||||
|
||||
pub(crate) async fn review_approval_request_with_cancel(
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
context: impl Into<GuardianReviewContext>,
|
||||
review_id: String,
|
||||
request: GuardianApprovalRequest,
|
||||
retry_reason: Option<String>,
|
||||
@@ -675,7 +677,7 @@ pub(crate) async fn review_approval_request_with_cancel(
|
||||
) -> ReviewDecision {
|
||||
run_guardian_review(
|
||||
Arc::clone(session),
|
||||
Arc::clone(turn),
|
||||
context.into(),
|
||||
review_id,
|
||||
request,
|
||||
ApprovalRequestReasons {
|
||||
@@ -689,12 +691,13 @@ pub(crate) async fn review_approval_request_with_cancel(
|
||||
|
||||
pub(crate) fn spawn_approval_request_review(
|
||||
session: Arc<Session>,
|
||||
turn: Arc<TurnContext>,
|
||||
context: impl Into<GuardianReviewContext>,
|
||||
review_id: String,
|
||||
request: GuardianApprovalRequest,
|
||||
retry_reason: Option<String>,
|
||||
options: GuardianReviewOptions,
|
||||
) -> oneshot::Receiver<ReviewDecision> {
|
||||
let context = context.into();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let spawn_result = std::thread::Builder::new()
|
||||
.name("codex-approval-review".to_string())
|
||||
@@ -710,7 +713,7 @@ pub(crate) fn spawn_approval_request_review(
|
||||
};
|
||||
let decision = runtime.block_on(review_approval_request_with_cancel(
|
||||
&session,
|
||||
&turn,
|
||||
context,
|
||||
review_id,
|
||||
request,
|
||||
retry_reason,
|
||||
@@ -842,13 +845,14 @@ pub(super) async fn guardian_review_session_config(
|
||||
/// rules.
|
||||
async fn run_guardian_review_session_before_deadline(
|
||||
session: Arc<Session>,
|
||||
turn: Arc<TurnContext>,
|
||||
context: GuardianReviewContext,
|
||||
request: GuardianApprovalRequest,
|
||||
reasons: ApprovalRequestReasons,
|
||||
schema: serde_json::Value,
|
||||
external_cancel: Option<CancellationToken>,
|
||||
deadline: Instant,
|
||||
) -> (GuardianReviewOutcome, GuardianReviewAnalyticsResult) {
|
||||
let turn = context.turn();
|
||||
let session_config = match guardian_review_session_config(session.as_ref(), turn.as_ref()).await
|
||||
{
|
||||
Ok(session_config) => session_config,
|
||||
@@ -864,7 +868,7 @@ async fn run_guardian_review_session_before_deadline(
|
||||
.guardian_review_session
|
||||
.run_review(GuardianReviewSessionParams {
|
||||
parent_session: Arc::clone(&session),
|
||||
parent_turn: turn.clone(),
|
||||
parent_context: context.clone(),
|
||||
spawn_config: session_config.spawn_config,
|
||||
request,
|
||||
reasons,
|
||||
@@ -936,20 +940,21 @@ async fn run_guardian_review_session_before_deadline(
|
||||
|
||||
pub(super) async fn run_guardian_review_session_with_retry(
|
||||
session: Arc<Session>,
|
||||
turn: Arc<TurnContext>,
|
||||
context: impl Into<GuardianReviewContext>,
|
||||
request: GuardianApprovalRequest,
|
||||
reasons: ApprovalRequestReasons,
|
||||
schema: serde_json::Value,
|
||||
external_cancel: Option<CancellationToken>,
|
||||
max_attempts: i64,
|
||||
) -> (GuardianReviewOutcome, GuardianReviewAnalyticsResult) {
|
||||
let context = context.into();
|
||||
assert!(max_attempts > 0, "guardian review must run at least once");
|
||||
let deadline = Instant::now() + GUARDIAN_REVIEW_TIMEOUT;
|
||||
let mut attempt_count = 1;
|
||||
loop {
|
||||
let (outcome, mut analytics_result) = run_guardian_review_session_before_deadline(
|
||||
Arc::clone(&session),
|
||||
Arc::clone(&turn),
|
||||
context.clone(),
|
||||
request.clone(),
|
||||
reasons.clone(),
|
||||
schema.clone(),
|
||||
|
||||
@@ -56,6 +56,7 @@ use codex_utils_path_uri::PathUri;
|
||||
use super::ApprovalRequestReasons;
|
||||
use super::GUARDIAN_REVIEWER_NAME;
|
||||
use super::GuardianApprovalRequest;
|
||||
use super::GuardianReviewContext;
|
||||
use super::prompt::BUNDLED_GUARDIAN_POLICY;
|
||||
use super::prompt::BUNDLED_GUARDIAN_POLICY_TEMPLATE;
|
||||
use super::prompt::GuardianPromptMode;
|
||||
@@ -79,7 +80,7 @@ pub(crate) enum GuardianReviewSessionOutcome {
|
||||
|
||||
pub(crate) struct GuardianReviewSessionParams {
|
||||
pub(crate) parent_session: Arc<Session>,
|
||||
pub(crate) parent_turn: Arc<TurnContext>,
|
||||
pub(crate) parent_context: GuardianReviewContext,
|
||||
pub(crate) spawn_config: Config,
|
||||
pub(crate) request: GuardianApprovalRequest,
|
||||
pub(crate) reasons: ApprovalRequestReasons,
|
||||
@@ -358,7 +359,7 @@ impl GuardianReviewSessionManager {
|
||||
let spawn_cancel_guard = spawn_cancel_token.clone().drop_guard();
|
||||
let review_session = spawn_guardian_review_session(
|
||||
&parent_session,
|
||||
&parent_turn,
|
||||
&GuardianReviewContext::from(parent_turn),
|
||||
spawn_config,
|
||||
reuse_key,
|
||||
spawn_cancel_token.clone(),
|
||||
@@ -459,7 +460,7 @@ impl GuardianReviewSessionManager {
|
||||
&spawn_cancel_token,
|
||||
Box::pin(spawn_guardian_review_session(
|
||||
¶ms.parent_session,
|
||||
¶ms.parent_turn,
|
||||
¶ms.parent_context,
|
||||
params.spawn_config.clone(),
|
||||
next_reuse_key.clone(),
|
||||
spawn_cancel_token.clone(),
|
||||
@@ -678,7 +679,7 @@ impl GuardianReviewSessionManager {
|
||||
&spawn_cancel_token,
|
||||
Box::pin(spawn_guardian_review_session(
|
||||
¶ms.parent_session,
|
||||
¶ms.parent_turn,
|
||||
¶ms.parent_context,
|
||||
fork_config,
|
||||
reuse_key,
|
||||
spawn_cancel_token.clone(),
|
||||
@@ -721,7 +722,7 @@ impl GuardianReviewSessionManager {
|
||||
|
||||
async fn spawn_guardian_review_session(
|
||||
parent_session: &Arc<Session>,
|
||||
parent_turn: &Arc<TurnContext>,
|
||||
parent_context: &GuardianReviewContext,
|
||||
spawn_config: Config,
|
||||
reuse_key: GuardianReviewSessionReuseKey,
|
||||
cancel_token: CancellationToken,
|
||||
@@ -746,7 +747,8 @@ async fn spawn_guardian_review_session(
|
||||
parent_session.services.auth_manager.clone(),
|
||||
parent_session.services.models_manager.clone(),
|
||||
Arc::clone(parent_session),
|
||||
Arc::clone(parent_turn),
|
||||
Arc::clone(parent_context.turn()),
|
||||
parent_context.environments().clone(),
|
||||
cancel_token.clone(),
|
||||
SubAgentSource::Other(GUARDIAN_REVIEWER_NAME.to_string()),
|
||||
initial_history,
|
||||
@@ -836,7 +838,7 @@ async fn run_review_on_session(
|
||||
|
||||
build_guardian_prompt_items_with_parent_turn(
|
||||
params.parent_session.as_ref(),
|
||||
Some(params.parent_turn.as_ref()),
|
||||
Some(¶ms.parent_context),
|
||||
params.reasons.clone(),
|
||||
params.request.clone(),
|
||||
prompt_mode,
|
||||
@@ -867,15 +869,15 @@ async fn run_review_on_session(
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let guardian_permission_profile = PermissionProfile::read_only();
|
||||
let parent_turn_environments = params.parent_turn.environments.to_selections();
|
||||
let parent_turn_environments = params.parent_context.environments().to_selections();
|
||||
// TODO(anp): Migrate guardian review thread settings to a PathUri fallback cwd so foreign
|
||||
// parent environments do not fall back to the host-native config cwd.
|
||||
let parent_turn_legacy_fallback_cwd = params
|
||||
.parent_turn
|
||||
.environments
|
||||
.parent_context
|
||||
.environments()
|
||||
.primary()
|
||||
.and_then(|environment| environment.cwd().to_abs_path().ok())
|
||||
.unwrap_or_else(|| params.parent_turn.config.cwd.clone());
|
||||
.unwrap_or_else(|| params.parent_context.turn().config.cwd.clone());
|
||||
|
||||
let submission = review_session.io.submit_with_trace(
|
||||
Op::UserInput {
|
||||
@@ -905,7 +907,7 @@ async fn run_review_on_session(
|
||||
},
|
||||
},
|
||||
/*trace*/ None,
|
||||
Some(params.parent_turn.sub_id.clone()),
|
||||
Some(params.parent_context.turn().sub_id.clone()),
|
||||
);
|
||||
let submit_result = run_before_review_deadline(
|
||||
deadline,
|
||||
@@ -1308,7 +1310,7 @@ mod tests {
|
||||
|
||||
GuardianReviewSessionParams {
|
||||
parent_session: Arc::new(session),
|
||||
parent_turn: Arc::new(turn),
|
||||
parent_context: GuardianReviewContext::from(Arc::new(turn)),
|
||||
spawn_config,
|
||||
request: GuardianApprovalRequest::Shell {
|
||||
id: "shell-1".to_string(),
|
||||
@@ -1338,7 +1340,10 @@ mod tests {
|
||||
let params = test_review_params().await;
|
||||
let manager = GuardianReviewSessionManager::default();
|
||||
manager
|
||||
.initialize(params.parent_session, params.parent_turn)
|
||||
.initialize(
|
||||
params.parent_session,
|
||||
Arc::clone(params.parent_context.turn()),
|
||||
)
|
||||
.await
|
||||
.expect("initialize Guardian session");
|
||||
let mode = manager
|
||||
|
||||
@@ -487,10 +487,11 @@ async fn build_guardian_prompt_full_mode_preserves_initial_review_format() -> an
|
||||
async fn build_guardian_prompt_prefers_retry_reason_over_approval_reason() -> anyhow::Result<()> {
|
||||
let (session, turn) = guardian_test_session_and_turn_with_base_url("http://localhost").await;
|
||||
seed_guardian_parent_history(&session, &turn).await;
|
||||
let context = GuardianReviewContext::from(&turn);
|
||||
|
||||
let prompt = build_guardian_prompt_items_with_parent_turn(
|
||||
session.as_ref(),
|
||||
Some(turn.as_ref()),
|
||||
Some(&context),
|
||||
ApprovalRequestReasons {
|
||||
approval: Some("A policy rule requires approval.".to_string()),
|
||||
retry: Some("The sandbox blocked the initial command.".to_string()),
|
||||
@@ -518,6 +519,7 @@ async fn build_guardian_prompt_prefers_retry_reason_over_approval_reason() -> an
|
||||
async fn build_guardian_prompt_truncates_oversized_approval_reason() -> anyhow::Result<()> {
|
||||
let (session, turn) = guardian_test_session_and_turn_with_base_url("http://localhost").await;
|
||||
seed_guardian_parent_history(&session, &turn).await;
|
||||
let context = GuardianReviewContext::from(&turn);
|
||||
let approval_reason = format!("policy-start {} policy-end", "x".repeat(10_000));
|
||||
let expected_reason = codex_utils_output_truncation::truncate_text(
|
||||
&approval_reason,
|
||||
@@ -526,7 +528,7 @@ async fn build_guardian_prompt_truncates_oversized_approval_reason() -> anyhow::
|
||||
|
||||
let prompt = build_guardian_prompt_items_with_parent_turn(
|
||||
session.as_ref(),
|
||||
Some(turn.as_ref()),
|
||||
Some(&context),
|
||||
ApprovalRequestReasons {
|
||||
approval: Some(approval_reason),
|
||||
retry: None,
|
||||
@@ -618,10 +620,11 @@ async fn build_guardian_prompt_includes_parent_turn_denied_reads() -> anyhow::Re
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
seed_guardian_parent_history(&session, &turn).await;
|
||||
let context = GuardianReviewContext::from(&turn);
|
||||
|
||||
let prompt = build_guardian_prompt_items_with_parent_turn(
|
||||
session.as_ref(),
|
||||
Some(turn.as_ref()),
|
||||
Some(&context),
|
||||
ApprovalRequestReasons {
|
||||
approval: None,
|
||||
retry: Some("Sandbox denied reading /repo/private/secret.txt.".to_string()),
|
||||
|
||||
@@ -1363,7 +1363,7 @@ async fn maybe_request_mcp_tool_approval(
|
||||
let review_id = new_guardian_review_id();
|
||||
let decision = review_approval_request(
|
||||
sess,
|
||||
turn_context,
|
||||
step_context,
|
||||
review_id.clone(),
|
||||
build_guardian_mcp_tool_review_request(call_id, invocation, Some(metadata)),
|
||||
Default::default(),
|
||||
|
||||
@@ -2478,12 +2478,14 @@ impl Session {
|
||||
)]
|
||||
pub(crate) async fn request_permissions_for_environment(
|
||||
self: &Arc<Self>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
review_context: impl Into<crate::guardian::GuardianReviewContext>,
|
||||
call_id: String,
|
||||
args: RequestPermissionsArgs,
|
||||
environment: TurnEnvironmentSelection,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Option<RequestPermissionsResponse> {
|
||||
let review_context = review_context.into();
|
||||
let turn_context = review_context.turn();
|
||||
match turn_context.as_ref().approval_policy() {
|
||||
AskForApproval::Never => {
|
||||
return Some(RequestPermissionsResponse {
|
||||
@@ -2527,7 +2529,6 @@ impl Session {
|
||||
};
|
||||
let review_id = crate::guardian::new_guardian_review_id();
|
||||
let session = Arc::clone(self);
|
||||
let turn = Arc::clone(turn_context);
|
||||
let request = crate::guardian::GuardianApprovalRequest::RequestPermissions {
|
||||
id: call_id,
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
@@ -2536,7 +2537,7 @@ impl Session {
|
||||
};
|
||||
let review_rx = crate::guardian::spawn_approval_request_review(
|
||||
session,
|
||||
turn,
|
||||
review_context.clone(),
|
||||
review_id,
|
||||
request,
|
||||
/*retry_reason*/ None,
|
||||
|
||||
@@ -1291,7 +1291,7 @@ async fn danger_full_access_tool_attempts_do_not_enforce_managed_network() -> an
|
||||
let mut tool = ProbeToolRuntime::default();
|
||||
let tool_ctx = crate::tools::sandboxing::ToolCtx {
|
||||
session: Arc::clone(&session),
|
||||
turn: Arc::clone(&turn),
|
||||
step_context: StepContext::for_test(Arc::clone(&turn)),
|
||||
call_id: "probe-call".to_string(),
|
||||
tool_name: codex_tools::ToolName::plain("probe"),
|
||||
};
|
||||
@@ -6477,7 +6477,7 @@ async fn request_permissions_emits_event_when_granular_policy_allows_requests()
|
||||
.selection();
|
||||
session
|
||||
.request_permissions_for_environment(
|
||||
&turn_context,
|
||||
turn_context.as_ref(),
|
||||
call_id,
|
||||
codex_protocol::request_permissions::RequestPermissionsArgs {
|
||||
environment_id: None,
|
||||
@@ -6727,7 +6727,7 @@ async fn request_permissions_response_materializes_session_cwd_grants_before_rec
|
||||
.selection();
|
||||
session
|
||||
.request_permissions_for_environment(
|
||||
&turn_context,
|
||||
turn_context.as_ref(),
|
||||
call_id,
|
||||
codex_protocol::request_permissions::RequestPermissionsArgs {
|
||||
environment_id: None,
|
||||
@@ -6819,7 +6819,7 @@ async fn request_permissions_is_auto_denied_when_granular_policy_blocks_tool_req
|
||||
.selection();
|
||||
let response = session
|
||||
.request_permissions_for_environment(
|
||||
&turn_context,
|
||||
turn_context.as_ref(),
|
||||
call_id,
|
||||
codex_protocol::request_permissions::RequestPermissionsArgs {
|
||||
environment_id: None,
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::guardian::routes_approval_to_guardian_with_reviewer;
|
||||
use crate::hook_runtime::run_permission_request_hooks;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::step_context::StepContext;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::tools::flat_tool_name;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
@@ -35,7 +36,7 @@ use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ApprovalContext {
|
||||
pub(crate) turn: Arc<TurnContext>,
|
||||
pub(crate) step_context: Arc<StepContext>,
|
||||
pub(crate) call_id: String,
|
||||
pub(crate) tool_name: ToolName,
|
||||
pub(crate) strict_auto_review: bool,
|
||||
@@ -308,7 +309,7 @@ impl Session {
|
||||
// 2. If StrictAutoReview || Guardian enabled, then Guardian. Else, user.
|
||||
let resolution = match run_permission_request_hooks(
|
||||
self,
|
||||
&ctx.turn,
|
||||
&ctx.step_context.turn,
|
||||
permission_request_run_id.as_deref().unwrap_or(&ctx.call_id),
|
||||
action.permission_request_payload(),
|
||||
)
|
||||
@@ -336,7 +337,7 @@ impl Session {
|
||||
let reviewer = if ctx.strict_auto_review {
|
||||
ApprovalReviewer::Guardian
|
||||
} else {
|
||||
ApprovalReviewer::for_turn(&ctx.turn)
|
||||
ApprovalReviewer::for_turn(&ctx.step_context.turn)
|
||||
};
|
||||
|
||||
let decision = match reviewer {
|
||||
@@ -368,7 +369,7 @@ impl Session {
|
||||
|
||||
review_approval_request(
|
||||
self,
|
||||
&ctx.turn,
|
||||
&ctx.step_context,
|
||||
review_id,
|
||||
action,
|
||||
ApprovalRequestReasons {
|
||||
@@ -422,7 +423,7 @@ impl Session {
|
||||
.or_else(|| justification.clone());
|
||||
with_cached_approval(&self.services, tool_name, action.cache_keys(), || async {
|
||||
self.request_command_approval(
|
||||
&ctx.turn,
|
||||
&ctx.step_context.turn,
|
||||
ctx.call_id.clone(),
|
||||
/*approval_id*/ None,
|
||||
Some(environment_id.clone()),
|
||||
@@ -454,7 +455,7 @@ impl Session {
|
||||
if reason.is_some() {
|
||||
return self
|
||||
.request_patch_approval(
|
||||
&ctx.turn,
|
||||
&ctx.step_context.turn,
|
||||
ctx.call_id.clone(),
|
||||
changes.as_ref().clone(),
|
||||
reason,
|
||||
@@ -468,7 +469,7 @@ impl Session {
|
||||
action.cache_keys(),
|
||||
|| async {
|
||||
self.request_patch_approval(
|
||||
&ctx.turn,
|
||||
&ctx.step_context.turn,
|
||||
ctx.call_id.clone(),
|
||||
changes.as_ref().clone(),
|
||||
/*reason*/ None,
|
||||
@@ -490,7 +491,7 @@ fn record_resolution(ctx: &ApprovalContext, resolution: &ApprovalResolution) {
|
||||
ApprovalResolutionSource::User => ToolDecisionSource::User,
|
||||
};
|
||||
let tool_name = flat_tool_name(&ctx.tool_name);
|
||||
ctx.turn.session_telemetry.tool_decision(
|
||||
ctx.step_context.turn.session_telemetry.tool_decision(
|
||||
tool_name.as_ref(),
|
||||
&ctx.call_id,
|
||||
&resolution.decision,
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::apply_patch;
|
||||
use crate::apply_patch::convert_apply_patch_to_protocol;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::step_context::StepContext;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
use crate::tools::context::ApplyPatchToolOutput;
|
||||
@@ -397,7 +398,7 @@ impl ApplyPatchHandler {
|
||||
codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {
|
||||
let tool_ctx = ToolCtx {
|
||||
session,
|
||||
turn,
|
||||
step_context: Arc::clone(&step_context),
|
||||
call_id,
|
||||
tool_name,
|
||||
};
|
||||
@@ -487,11 +488,12 @@ pub(crate) async fn intercept_apply_patch(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
turn_environment: TurnEnvironment,
|
||||
session: Arc<Session>,
|
||||
turn: Arc<TurnContext>,
|
||||
step_context: Arc<StepContext>,
|
||||
tracker: Option<&SharedTurnDiffTracker>,
|
||||
call_id: &str,
|
||||
tool_name: &str,
|
||||
) -> Result<Option<FunctionToolOutput>, FunctionCallError> {
|
||||
let turn = &step_context.turn;
|
||||
let sandbox =
|
||||
turn.file_system_sandbox_context(/*additional_permissions*/ None, &turn_environment);
|
||||
match codex_apply_patch::maybe_parse_apply_patch_verified(command, cwd, fs, Some(&sandbox))
|
||||
@@ -500,7 +502,7 @@ pub(crate) async fn intercept_apply_patch(
|
||||
codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {
|
||||
let tool_ctx = ToolCtx {
|
||||
session,
|
||||
turn,
|
||||
step_context,
|
||||
call_id: call_id.to_string(),
|
||||
tool_name: ToolName::plain(tool_name),
|
||||
};
|
||||
@@ -533,7 +535,7 @@ async fn execute_verified_patch(
|
||||
.await
|
||||
.unwrap_or_else(|_| patch_permissions_without_path_matching(&action));
|
||||
let apply = apply_patch::prepare_apply_patch(
|
||||
tool_ctx.turn.as_ref(),
|
||||
tool_ctx.step_context.turn.as_ref(),
|
||||
turn_environment.permission_profile(),
|
||||
&file_system_sandbox_policy,
|
||||
action,
|
||||
@@ -546,7 +548,7 @@ async fn execute_verified_patch(
|
||||
);
|
||||
let event_ctx = ToolEventCtx::new(
|
||||
tool_ctx.session.as_ref(),
|
||||
tool_ctx.turn.as_ref(),
|
||||
tool_ctx.step_context.turn.as_ref(),
|
||||
&tool_ctx.call_id,
|
||||
tracker,
|
||||
);
|
||||
@@ -568,8 +570,8 @@ async fn execute_verified_patch(
|
||||
&mut runtime,
|
||||
&request,
|
||||
&tool_ctx,
|
||||
tool_ctx.turn.as_ref(),
|
||||
tool_ctx.turn.approval_policy(),
|
||||
tool_ctx.step_context.turn.as_ref(),
|
||||
tool_ctx.step_context.turn.approval_policy(),
|
||||
)
|
||||
.await
|
||||
.map(|result| result.output);
|
||||
@@ -579,7 +581,7 @@ async fn execute_verified_patch(
|
||||
};
|
||||
let event_ctx = ToolEventCtx::new(
|
||||
tool_ctx.session.as_ref(),
|
||||
tool_ctx.turn.as_ref(),
|
||||
tool_ctx.step_context.turn.as_ref(),
|
||||
&tool_ctx.call_id,
|
||||
tracker,
|
||||
);
|
||||
|
||||
@@ -46,7 +46,6 @@ impl RequestPermissionsHandler {
|
||||
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError> {
|
||||
let ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
step_context,
|
||||
cancellation_token,
|
||||
call_id,
|
||||
@@ -94,7 +93,7 @@ impl RequestPermissionsHandler {
|
||||
|
||||
let response = session
|
||||
.request_permissions_for_environment(
|
||||
&turn,
|
||||
&step_context,
|
||||
call_id,
|
||||
args,
|
||||
turn_environment.selection(),
|
||||
|
||||
@@ -7,7 +7,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use crate::exec::ExecParams;
|
||||
use crate::exec_policy::ExecApprovalRequest;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::session::step_context::StepContext;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
use crate::shell::ShellType;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
@@ -53,7 +53,7 @@ struct RunExecLikeArgs {
|
||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
prefix_rule: Option<Vec<String>>,
|
||||
session: Arc<crate::session::session::Session>,
|
||||
turn: Arc<TurnContext>,
|
||||
step_context: Arc<StepContext>,
|
||||
turn_environment: TurnEnvironment,
|
||||
tracker: crate::tools::context::SharedTurnDiffTracker,
|
||||
call_id: String,
|
||||
@@ -70,12 +70,13 @@ async fn run_exec_like(args: RunExecLikeArgs) -> Result<FunctionToolOutput, Func
|
||||
additional_permissions,
|
||||
prefix_rule,
|
||||
session,
|
||||
turn,
|
||||
step_context,
|
||||
turn_environment,
|
||||
tracker,
|
||||
call_id,
|
||||
shell_runtime_backend,
|
||||
} = args;
|
||||
let turn = Arc::clone(&step_context.turn);
|
||||
|
||||
let fs = turn_environment.environment.get_filesystem();
|
||||
|
||||
@@ -145,7 +146,7 @@ async fn run_exec_like(args: RunExecLikeArgs) -> Result<FunctionToolOutput, Func
|
||||
fs.as_ref(),
|
||||
turn_environment.clone(),
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
Arc::clone(&step_context),
|
||||
Some(&tracker),
|
||||
&call_id,
|
||||
tool_name.name.as_str(),
|
||||
@@ -213,7 +214,7 @@ async fn run_exec_like(args: RunExecLikeArgs) -> Result<FunctionToolOutput, Func
|
||||
let mut runtime = ShellRuntime::for_shell_command(shell_runtime_backend);
|
||||
let tool_ctx = ToolCtx {
|
||||
session: session.clone(),
|
||||
turn: turn.clone(),
|
||||
step_context,
|
||||
call_id: call_id.clone(),
|
||||
tool_name,
|
||||
};
|
||||
|
||||
@@ -230,7 +230,7 @@ impl ShellCommandHandler {
|
||||
additional_permissions: params.additional_permissions.clone(),
|
||||
prefix_rule,
|
||||
session,
|
||||
turn,
|
||||
step_context,
|
||||
turn_environment,
|
||||
tracker,
|
||||
call_id,
|
||||
|
||||
@@ -128,7 +128,8 @@ impl ExecCommandHandler {
|
||||
};
|
||||
|
||||
let manager: &UnifiedExecProcessManager = &session.services.unified_exec_manager;
|
||||
let context = UnifiedExecContext::new(session.clone(), turn.clone(), call_id.clone());
|
||||
let context =
|
||||
UnifiedExecContext::new(session.clone(), step_context.clone(), call_id.clone());
|
||||
let environment_args: ExecCommandEnvironmentArgs = parse_arguments(&arguments)?;
|
||||
let Some(turn_environment) = resolve_tool_environment(
|
||||
&step_context.environments,
|
||||
@@ -194,7 +195,7 @@ impl ExecCommandHandler {
|
||||
if let Some(native_cwd) = native_cwd.as_ref() {
|
||||
maybe_emit_implicit_skill_invocation(
|
||||
session.as_ref(),
|
||||
context.turn.as_ref(),
|
||||
context.step_context.turn.as_ref(),
|
||||
&hook_command,
|
||||
native_cwd,
|
||||
)
|
||||
@@ -292,11 +293,11 @@ impl ExecCommandHandler {
|
||||
.requests_sandbox_override()
|
||||
&& !effective_additional_permissions.permissions_preapproved
|
||||
&& !matches!(
|
||||
context.turn.approval_policy(),
|
||||
context.step_context.turn.approval_policy(),
|
||||
codex_protocol::protocol::AskForApproval::OnRequest
|
||||
)
|
||||
{
|
||||
let approval_policy = context.turn.approval_policy();
|
||||
let approval_policy = context.step_context.turn.approval_policy();
|
||||
manager.release_process_id(process_id).await;
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"approval policy is {approval_policy:?}; reject command — you cannot ask for escalated permissions if the approval policy is {approval_policy:?}"
|
||||
@@ -312,7 +313,7 @@ impl ExecCommandHandler {
|
||||
|| {
|
||||
normalize_and_validate_additional_permissions(
|
||||
additional_permissions_allowed,
|
||||
context.turn.approval_policy(),
|
||||
context.step_context.turn.approval_policy(),
|
||||
effective_additional_permissions.sandbox_permissions,
|
||||
effective_additional_permissions.additional_permissions,
|
||||
effective_additional_permissions.permissions_preapproved,
|
||||
@@ -334,7 +335,7 @@ impl ExecCommandHandler {
|
||||
fs.as_ref(),
|
||||
turn_environment.clone(),
|
||||
context.session.clone(),
|
||||
context.turn.clone(),
|
||||
Arc::clone(&context.step_context),
|
||||
Some(&tracker),
|
||||
&context.call_id,
|
||||
"exec_command",
|
||||
@@ -371,7 +372,7 @@ impl ExecCommandHandler {
|
||||
sandbox_cwd: native_environment_cwd,
|
||||
turn_environment: turn_environment.clone(),
|
||||
shell_mode,
|
||||
network: context.turn.network.clone(),
|
||||
network: context.step_context.turn.network.clone(),
|
||||
tty,
|
||||
sandbox_permissions: effective_additional_permissions.sandbox_permissions,
|
||||
additional_permissions: normalized_additional_permissions,
|
||||
|
||||
@@ -63,7 +63,7 @@ impl ToolOrchestrator {
|
||||
{
|
||||
let network_approval = match begin_network_approval(
|
||||
&tool_ctx.session,
|
||||
&tool_ctx.turn.sub_id,
|
||||
&tool_ctx.step_context.turn.sub_id,
|
||||
managed_network_active,
|
||||
tool.network_approval_spec(req, tool_ctx),
|
||||
)
|
||||
@@ -75,7 +75,7 @@ impl ToolOrchestrator {
|
||||
|
||||
let attempt_tool_ctx = ToolCtx {
|
||||
session: tool_ctx.session.clone(),
|
||||
turn: tool_ctx.turn.clone(),
|
||||
step_context: Arc::clone(&tool_ctx.step_context),
|
||||
call_id: tool_ctx.call_id.clone(),
|
||||
tool_name: tool_ctx.tool_name.clone(),
|
||||
};
|
||||
@@ -172,7 +172,7 @@ impl ToolOrchestrator {
|
||||
ToolError::Rejected(format!("could not prepare approval action: {err}"))
|
||||
})?;
|
||||
let approval_ctx = ApprovalContext {
|
||||
turn: Arc::clone(&tool_ctx.turn),
|
||||
step_context: Arc::clone(&tool_ctx.step_context),
|
||||
call_id: tool_ctx.call_id.clone(),
|
||||
tool_name: tool_ctx.tool_name.clone(),
|
||||
strict_auto_review,
|
||||
@@ -204,7 +204,7 @@ impl ToolOrchestrator {
|
||||
ToolError::Rejected(format!("could not prepare approval action: {err}"))
|
||||
})?;
|
||||
let approval_ctx = ApprovalContext {
|
||||
turn: Arc::clone(&tool_ctx.turn),
|
||||
step_context: Arc::clone(&tool_ctx.step_context),
|
||||
call_id: tool_ctx.call_id.clone(),
|
||||
tool_name: tool_ctx.tool_name.clone(),
|
||||
strict_auto_review,
|
||||
@@ -400,7 +400,7 @@ impl ToolOrchestrator {
|
||||
ToolError::Rejected(format!("could not prepare approval action: {err}"))
|
||||
})?;
|
||||
let approval_ctx = ApprovalContext {
|
||||
turn: Arc::clone(&tool_ctx.turn),
|
||||
step_context: Arc::clone(&tool_ctx.step_context),
|
||||
call_id: tool_ctx.call_id.clone(),
|
||||
tool_name: tool_ctx.tool_name.clone(),
|
||||
strict_auto_review,
|
||||
|
||||
@@ -101,7 +101,7 @@ impl ShellRuntime {
|
||||
|
||||
fn stdout_stream(ctx: &ToolCtx) -> Option<crate::exec::StdoutStream> {
|
||||
Some(crate::exec::StdoutStream {
|
||||
sub_id: ctx.turn.sub_id.clone(),
|
||||
sub_id: ctx.step_context.turn.sub_id.clone(),
|
||||
call_id: ctx.call_id.clone(),
|
||||
tx_event: ctx.session.get_tx_event(),
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::exec::ExecExpiration;
|
||||
use crate::exec::cancel_when_either;
|
||||
use crate::exec::is_likely_sandbox_denied;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::GuardianReviewContext;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
@@ -199,8 +200,8 @@ pub(super) async fn try_run_zsh_fork(
|
||||
arg0,
|
||||
sandbox_policy_cwd,
|
||||
windows_sandbox_workspace_roots,
|
||||
codex_linux_sandbox_exe: ctx.turn.config.codex_linux_sandbox_exe.clone(),
|
||||
use_legacy_landlock: ctx.turn.config.features.use_legacy_landlock(),
|
||||
codex_linux_sandbox_exe: ctx.step_context.turn.config.codex_linux_sandbox_exe.clone(),
|
||||
use_legacy_landlock: ctx.step_context.turn.config.features.use_legacy_landlock(),
|
||||
};
|
||||
let main_execve_wrapper_exe = ctx
|
||||
.session
|
||||
@@ -234,11 +235,11 @@ pub(super) async fn try_run_zsh_fork(
|
||||
let escalation_policy = CoreShellActionProvider {
|
||||
policy: Arc::clone(&exec_policy),
|
||||
session: Arc::clone(&ctx.session),
|
||||
turn: Arc::clone(&ctx.turn),
|
||||
review_context: GuardianReviewContext::from(&ctx.step_context),
|
||||
call_id: ctx.call_id.clone(),
|
||||
environment_id: req.turn_environment.environment_id.clone(),
|
||||
tool_name: GuardianCommandSource::Shell,
|
||||
approval_policy: ctx.turn.approval_policy(),
|
||||
approval_policy: ctx.step_context.turn.approval_policy(),
|
||||
permission_profile: command_executor.permission_profile.clone(),
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
approval_sandbox_permissions,
|
||||
@@ -314,17 +315,17 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
|
||||
arg0: exec_request.arg0.clone(),
|
||||
sandbox_policy_cwd,
|
||||
windows_sandbox_workspace_roots: exec_request.windows_sandbox_workspace_roots.clone(),
|
||||
codex_linux_sandbox_exe: ctx.turn.config.codex_linux_sandbox_exe.clone(),
|
||||
use_legacy_landlock: ctx.turn.config.features.use_legacy_landlock(),
|
||||
codex_linux_sandbox_exe: ctx.step_context.turn.config.codex_linux_sandbox_exe.clone(),
|
||||
use_legacy_landlock: ctx.step_context.turn.config.features.use_legacy_landlock(),
|
||||
};
|
||||
let escalation_policy = CoreShellActionProvider {
|
||||
policy: Arc::clone(&exec_policy),
|
||||
session: Arc::clone(&ctx.session),
|
||||
turn: Arc::clone(&ctx.turn),
|
||||
review_context: GuardianReviewContext::from(&ctx.step_context),
|
||||
call_id: ctx.call_id.clone(),
|
||||
environment_id: req.turn_environment.environment_id.clone(),
|
||||
tool_name: GuardianCommandSource::UnifiedExec,
|
||||
approval_policy: ctx.turn.approval_policy(),
|
||||
approval_policy: ctx.step_context.turn.approval_policy(),
|
||||
permission_profile: exec_request.permission_profile.clone(),
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
approval_sandbox_permissions: approval_sandbox_permissions(
|
||||
@@ -354,7 +355,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
|
||||
struct CoreShellActionProvider {
|
||||
policy: Arc<RwLock<Policy>>,
|
||||
session: Arc<crate::session::session::Session>,
|
||||
turn: Arc<crate::session::turn_context::TurnContext>,
|
||||
review_context: GuardianReviewContext,
|
||||
call_id: String,
|
||||
environment_id: String,
|
||||
tool_name: GuardianCommandSource,
|
||||
@@ -442,7 +443,8 @@ impl CoreShellActionProvider {
|
||||
let command = join_program_and_argv(program, argv);
|
||||
let workdir = workdir.clone();
|
||||
let session = self.session.clone();
|
||||
let turn = self.turn.clone();
|
||||
let review_context = self.review_context.clone();
|
||||
let turn = Arc::clone(review_context.turn());
|
||||
let call_id = self.call_id.clone();
|
||||
let approval_id = Some(Uuid::new_v4().to_string());
|
||||
let environment_id = Some(self.environment_id.clone());
|
||||
@@ -477,7 +479,7 @@ impl CoreShellActionProvider {
|
||||
if let Some(review_id) = guardian_review_id {
|
||||
return review_approval_request(
|
||||
&session,
|
||||
&turn,
|
||||
review_context,
|
||||
review_id,
|
||||
GuardianApprovalRequest::Execve {
|
||||
id: call_id.clone(),
|
||||
@@ -615,7 +617,7 @@ impl CoreShellActionProvider {
|
||||
InterceptedExecPolicyContext {
|
||||
approval_policy: self.approval_policy,
|
||||
permission_profile: self.permission_profile.clone(),
|
||||
windows_sandbox_level: self.turn.windows_sandbox_level,
|
||||
windows_sandbox_level: self.review_context.turn().windows_sandbox_level,
|
||||
sandbox_permissions: self.approval_sandbox_permissions,
|
||||
enable_shell_wrapper_parsing:
|
||||
ENABLE_INTERCEPTED_EXEC_POLICY_SHELL_WRAPPER_PARSING,
|
||||
|
||||
@@ -8,6 +8,7 @@ use super::extract_shell_script;
|
||||
use super::join_program_and_argv;
|
||||
use super::map_exec_result;
|
||||
use crate::config::Constrained;
|
||||
use crate::guardian::GuardianReviewContext;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use anyhow::Context;
|
||||
@@ -425,7 +426,7 @@ async fn preapproved_additional_permissions_escalate_intercepted_exec() -> anyho
|
||||
let provider = CoreShellActionProvider {
|
||||
policy: Arc::new(RwLock::new(codex_execpolicy::Policy::empty())),
|
||||
session: Arc::new(session),
|
||||
turn: Arc::new(turn_context),
|
||||
review_context: GuardianReviewContext::from(Arc::new(turn_context)),
|
||||
call_id: "preapproved-additional-permissions".to_string(),
|
||||
environment_id: "local".to_string(),
|
||||
tool_name: GuardianCommandSource::Shell,
|
||||
@@ -563,7 +564,7 @@ async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Resul
|
||||
let provider = CoreShellActionProvider {
|
||||
policy: std::sync::Arc::new(RwLock::new(codex_execpolicy::Policy::empty())),
|
||||
session: std::sync::Arc::new(session),
|
||||
turn: std::sync::Arc::new(turn_context),
|
||||
review_context: GuardianReviewContext::from(Arc::new(turn_context)),
|
||||
call_id: "execve-hook-call".to_string(),
|
||||
environment_id: "local".to_string(),
|
||||
tool_name: GuardianCommandSource::Shell,
|
||||
@@ -773,7 +774,7 @@ prefix_rule(pattern = ["{cat_path_literal}"], decision = "allow")
|
||||
let provider = CoreShellActionProvider {
|
||||
policy: Arc::new(RwLock::new(policy)),
|
||||
session: Arc::new(session),
|
||||
turn: Arc::new(turn_context),
|
||||
review_context: GuardianReviewContext::from(Arc::new(turn_context)),
|
||||
call_id: "deny-read-prefix-allow".to_string(),
|
||||
environment_id: "local".to_string(),
|
||||
tool_name: GuardianCommandSource::Shell,
|
||||
@@ -809,7 +810,7 @@ async fn denied_reads_keep_granular_sandbox_rejection_for_escalation() -> anyhow
|
||||
let provider = CoreShellActionProvider {
|
||||
policy: Arc::new(RwLock::new(PolicyParser::new().build())),
|
||||
session: Arc::new(session),
|
||||
turn: Arc::new(turn_context),
|
||||
review_context: GuardianReviewContext::from(Arc::new(turn_context)),
|
||||
call_id: "deny-read-granular-sandbox-reject".to_string(),
|
||||
environment_id: "local".to_string(),
|
||||
tool_name: GuardianCommandSource::Shell,
|
||||
|
||||
@@ -274,7 +274,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
let mut launch = network.remote_launch_config().await.map_err(|err| {
|
||||
ToolError::Codex(CodexErr::Io(io::Error::other(err.to_string())))
|
||||
})?;
|
||||
if routes_approval_to_guardian(&ctx.turn)
|
||||
if routes_approval_to_guardian(&ctx.step_context.turn)
|
||||
&& network.remote_policy_decider().is_some()
|
||||
{
|
||||
let timeout = ctx
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::session::step_context::StepContext;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
use crate::state::SessionServices;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
@@ -348,7 +348,7 @@ pub(crate) trait Sandboxable {
|
||||
|
||||
pub(crate) struct ToolCtx {
|
||||
pub session: Arc<Session>,
|
||||
pub turn: Arc<TurnContext>,
|
||||
pub step_context: Arc<StepContext>,
|
||||
pub call_id: String,
|
||||
pub tool_name: ToolName,
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ pub(crate) fn start_streaming_output(
|
||||
} = process.output_handles().clone();
|
||||
|
||||
let session_ref = Arc::clone(&context.session);
|
||||
let turn_ref = Arc::clone(&context.turn);
|
||||
let turn_ref = Arc::clone(&context.step_context.turn);
|
||||
let call_id = context.call_id.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
|
||||
@@ -49,7 +49,11 @@ async fn streaming_output_harness() -> anyhow::Result<StreamingOutputHarness> {
|
||||
.await?,
|
||||
);
|
||||
let (session, turn, rx_event) = make_session_and_context_with_rx().await;
|
||||
let context = UnifiedExecContext::new(session, turn, "streaming-output-test".to_string());
|
||||
let context = UnifiedExecContext::new(
|
||||
session,
|
||||
crate::session::step_context::StepContext::for_test(turn),
|
||||
"streaming-output-test".to_string(),
|
||||
);
|
||||
let transcript = Arc::new(tokio::sync::Mutex::new(HeadTailBuffer::default()));
|
||||
start_streaming_output(&process, &context, Arc::clone(&transcript));
|
||||
|
||||
@@ -154,11 +158,11 @@ async fn exit_watcher_waits_for_late_network_denial_before_classifying_end() ->
|
||||
late_denial_armed_rx.await.expect("late denial armed");
|
||||
|
||||
#[allow(deprecated)]
|
||||
let cwd = context.turn.cwd.clone().into();
|
||||
let cwd = context.step_context.turn.cwd.clone().into();
|
||||
spawn_exit_watcher(
|
||||
Arc::clone(&process),
|
||||
Arc::clone(&context.session),
|
||||
Arc::clone(&context.turn),
|
||||
Arc::clone(&context.step_context.turn),
|
||||
context.call_id,
|
||||
vec!["proof".to_string()],
|
||||
cwd,
|
||||
|
||||
@@ -38,6 +38,7 @@ use tokio::sync::Mutex;
|
||||
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::step_context::StepContext;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
use crate::shell::ShellType;
|
||||
@@ -74,15 +75,15 @@ pub(crate) const MAX_UNIFIED_EXEC_PROCESSES: usize = 64;
|
||||
|
||||
pub(crate) struct UnifiedExecContext {
|
||||
pub session: Arc<Session>,
|
||||
pub turn: Arc<TurnContext>,
|
||||
pub step_context: Arc<StepContext>,
|
||||
pub call_id: String,
|
||||
}
|
||||
|
||||
impl UnifiedExecContext {
|
||||
pub fn new(session: Arc<Session>, turn: Arc<TurnContext>, call_id: String) -> Self {
|
||||
pub fn new(session: Arc<Session>, step_context: Arc<StepContext>, call_id: String) -> Self {
|
||||
Self {
|
||||
session,
|
||||
turn,
|
||||
step_context,
|
||||
call_id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,8 +124,11 @@ async fn exec_command_with_tty(
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
let context =
|
||||
UnifiedExecContext::new(Arc::clone(session), Arc::clone(turn), "call".to_string());
|
||||
let context = UnifiedExecContext::new(
|
||||
Arc::clone(session),
|
||||
crate::session::step_context::StepContext::for_test(Arc::clone(turn)),
|
||||
"call".to_string(),
|
||||
);
|
||||
let started_at = Instant::now();
|
||||
let process_started_alive = !process.has_exited() && process.exit_code().is_none();
|
||||
if process_started_alive {
|
||||
|
||||
@@ -344,7 +344,7 @@ async fn emit_failed_initial_exec_end_if_unstored(
|
||||
|
||||
emit_failed_exec_end_for_unified_exec(
|
||||
Arc::clone(&context.session),
|
||||
Arc::clone(&context.turn),
|
||||
Arc::clone(&context.step_context.turn),
|
||||
context.call_id.clone(),
|
||||
request.command.clone(),
|
||||
cwd,
|
||||
@@ -449,12 +449,13 @@ impl UnifiedExecProcessManager {
|
||||
let transcript = Arc::new(tokio::sync::Mutex::new(HeadTailBuffer::default()));
|
||||
let event_ctx = ToolEventCtx::new(
|
||||
context.session.as_ref(),
|
||||
context.turn.as_ref(),
|
||||
context.step_context.turn.as_ref(),
|
||||
&context.call_id,
|
||||
/*turn_diff_tracker*/ None,
|
||||
);
|
||||
let plugin_attribution = cwd.to_abs_path().ok().and_then(|cwd| {
|
||||
context
|
||||
.step_context
|
||||
.turn
|
||||
.plugin_attribution_for_command(&request.command, &cwd)
|
||||
});
|
||||
@@ -627,7 +628,7 @@ impl UnifiedExecProcessManager {
|
||||
let exit = exit_code.unwrap_or(-1);
|
||||
emit_exec_end_for_unified_exec(
|
||||
Arc::clone(&context.session),
|
||||
Arc::clone(&context.turn),
|
||||
Arc::clone(&context.step_context.turn),
|
||||
context.call_id.clone(),
|
||||
request.command.clone(),
|
||||
cwd.clone(),
|
||||
@@ -655,7 +656,12 @@ impl UnifiedExecProcessManager {
|
||||
chunk_id,
|
||||
wall_time,
|
||||
raw_output: collected,
|
||||
truncation_policy: context.turn.model_info.truncation_policy.into(),
|
||||
truncation_policy: context
|
||||
.step_context
|
||||
.turn
|
||||
.model_info
|
||||
.truncation_policy
|
||||
.into(),
|
||||
max_output_tokens: request.max_output_tokens,
|
||||
process_id: response_process_id,
|
||||
exit_code,
|
||||
@@ -952,7 +958,7 @@ impl UnifiedExecProcessManager {
|
||||
spawn_exit_watcher(
|
||||
Arc::clone(&process),
|
||||
Arc::clone(&context.session),
|
||||
Arc::clone(&context.turn),
|
||||
Arc::clone(&context.step_context.turn),
|
||||
context.call_id.clone(),
|
||||
command.to_vec(),
|
||||
cwd,
|
||||
@@ -1132,8 +1138,9 @@ impl UnifiedExecProcessManager {
|
||||
cwd: PathUri,
|
||||
context: &UnifiedExecContext,
|
||||
) -> Result<(UnifiedExecProcess, Option<DeferredNetworkApproval>), UnifiedExecError> {
|
||||
let turn = &context.step_context.turn;
|
||||
let local_policy_env = create_env(
|
||||
&context.turn.config.permissions.shell_environment_policy,
|
||||
&turn.config.permissions.shell_environment_policy,
|
||||
/*thread_id*/ None,
|
||||
);
|
||||
let mut env = local_policy_env.clone();
|
||||
@@ -1146,7 +1153,7 @@ impl UnifiedExecProcessManager {
|
||||
let env = apply_unified_exec_env(env);
|
||||
let exec_server_env_config = ExecServerEnvConfig {
|
||||
policy: exec_env_policy_from_shell_policy(
|
||||
&context.turn.config.permissions.shell_environment_policy,
|
||||
&turn.config.permissions.shell_environment_policy,
|
||||
),
|
||||
local_policy_env,
|
||||
};
|
||||
@@ -1158,9 +1165,9 @@ impl UnifiedExecProcessManager {
|
||||
.exec_policy
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &request.command,
|
||||
approval_policy: context.turn.approval_policy(),
|
||||
approval_policy: turn.approval_policy(),
|
||||
permission_profile: request.turn_environment.permission_profile().clone(),
|
||||
windows_sandbox_level: context.turn.windows_sandbox_level,
|
||||
windows_sandbox_level: turn.windows_sandbox_level,
|
||||
sandbox_permissions: if request.additional_permissions_preapproved {
|
||||
crate::sandboxing::SandboxPermissions::UseDefault
|
||||
} else {
|
||||
@@ -1180,8 +1187,7 @@ impl UnifiedExecProcessManager {
|
||||
turn_environment: request.turn_environment.clone(),
|
||||
env,
|
||||
exec_server_env_config: Some(exec_server_env_config),
|
||||
explicit_env_overrides: context
|
||||
.turn
|
||||
explicit_env_overrides: turn
|
||||
.config
|
||||
.permissions
|
||||
.shell_environment_policy
|
||||
@@ -1198,18 +1204,12 @@ impl UnifiedExecProcessManager {
|
||||
};
|
||||
let tool_ctx = ToolCtx {
|
||||
session: context.session.clone(),
|
||||
turn: context.turn.clone(),
|
||||
step_context: Arc::clone(&context.step_context),
|
||||
call_id: context.call_id.clone(),
|
||||
tool_name: ToolName::plain("exec_command"),
|
||||
};
|
||||
orchestrator
|
||||
.run(
|
||||
&mut runtime,
|
||||
&req,
|
||||
&tool_ctx,
|
||||
&context.turn,
|
||||
context.turn.approval_policy(),
|
||||
)
|
||||
.run(&mut runtime, &req, &tool_ctx, turn, turn.approval_policy())
|
||||
.await
|
||||
.map(|result| (result.output, result.deferred_network_approval))
|
||||
.map_err(|err| match err {
|
||||
|
||||
@@ -377,7 +377,7 @@ async fn failed_initial_end_for_unstored_process_uses_fallback_output() {
|
||||
let (session, turn, rx_event) = crate::session::tests::make_session_and_context_with_rx().await;
|
||||
let context = UnifiedExecContext::new(
|
||||
Arc::clone(&session),
|
||||
Arc::clone(&turn),
|
||||
crate::session::step_context::StepContext::for_test(Arc::clone(&turn)),
|
||||
"call-unified-denied".to_string(),
|
||||
);
|
||||
let request = ExecCommandRequest {
|
||||
|
||||
@@ -1390,6 +1390,178 @@ async fn deferred_executor_spawn_agent_inherits_ready_step_environments(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn deferred_executor_guardian_uses_newly_ready_step_environment() -> Result<()> {
|
||||
const WAIT_CALL_ID: &str = "wait-for-guardian-environment";
|
||||
const EXEC_CALL_ID: &str = "guardian-ready-environment-command";
|
||||
const DENIAL_RATIONALE: &str = "The remote environment policy denies this action.";
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let server = start_mock_server().await;
|
||||
let completed_response =
|
||||
|id: &str, item| sse(vec![ev_response_created(id), item, ev_completed(id)]);
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
completed_response(
|
||||
"resp-guardian-wait",
|
||||
ev_function_call(
|
||||
WAIT_CALL_ID,
|
||||
"wait_for_environment",
|
||||
&json!({ "environment_id": REMOTE_ENVIRONMENT_ID }).to_string(),
|
||||
),
|
||||
),
|
||||
completed_response(
|
||||
"resp-guardian-command",
|
||||
ev_function_call(
|
||||
EXEC_CALL_ID,
|
||||
"exec_command",
|
||||
&json!({
|
||||
"cmd": "printf guardian-should-not-run",
|
||||
"environment_id": REMOTE_ENVIRONMENT_ID,
|
||||
"sandbox_permissions": SandboxPermissions::RequireEscalated,
|
||||
"justification": "Review the newly ready remote environment.",
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
),
|
||||
completed_response(
|
||||
"resp-guardian-review",
|
||||
ev_assistant_message(
|
||||
"msg-guardian-review",
|
||||
&json!({ "outcome": "deny", "rationale": DENIAL_RATIONALE }).to_string(),
|
||||
),
|
||||
),
|
||||
completed_response(
|
||||
"resp-guardian-done",
|
||||
ev_assistant_message("msg-guardian-done", "done"),
|
||||
),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
let mut builder = test_codex()
|
||||
.with_exec_server_url(format!("ws://{}", listener.local_addr()?))
|
||||
.with_config(|config| {
|
||||
config.project_doc_max_bytes = 0;
|
||||
config.use_experimental_unified_exec_tool = true;
|
||||
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
|
||||
assert!(config.features.enable(Feature::DeferredExecutor).is_ok());
|
||||
assert!(config.features.enable(Feature::UnifiedExec).is_ok());
|
||||
});
|
||||
let (attach_tx, attach_rx) = tokio::sync::oneshot::channel();
|
||||
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
|
||||
let exec_server = tokio::spawn(serve_environment_with_agents_md(
|
||||
listener,
|
||||
"",
|
||||
attach_rx,
|
||||
shutdown_rx,
|
||||
));
|
||||
let test = timeout(
|
||||
Duration::from_secs(5),
|
||||
builder.build_with_remote_and_local_env(&server),
|
||||
)
|
||||
.await
|
||||
.context("thread startup should not wait for the remote environment")??;
|
||||
let remote_cwd = test.cwd.path().join("guardian-remote").abs();
|
||||
let local_cwd = test.cwd.path().abs();
|
||||
fs::create_dir_all(remote_cwd.as_path())?;
|
||||
let remote_denied_path = remote_cwd.canonicalize()?.join("private");
|
||||
let local_denied_path = local_cwd.canonicalize()?.join("private");
|
||||
let remote_selection = TurnEnvironmentSelection {
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&remote_cwd),
|
||||
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
|
||||
};
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry::new(
|
||||
FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
FileSystemAccessMode::Read,
|
||||
),
|
||||
FileSystemSandboxEntry::new(
|
||||
FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(Some("private".to_string())),
|
||||
},
|
||||
FileSystemAccessMode::Deny,
|
||||
),
|
||||
]),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
let (sandbox_policy, permission_profile) =
|
||||
turn_permission_fields(permission_profile, test.config.cwd.as_path());
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "review a command after the remote environment becomes ready".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: ThreadSettingsOverrides {
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
test.config.cwd.clone(),
|
||||
vec![remote_selection, local(local_cwd.clone())],
|
||||
)),
|
||||
sandbox_policy: Some(sandbox_policy),
|
||||
permission_profile,
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
wait_for_response_request_count(&responses, /*expected_count*/ 1).await;
|
||||
attach_tx.send(()).expect("attach remote environment");
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
|
||||
let requests = responses.requests();
|
||||
assert!(
|
||||
requests[1]
|
||||
.function_call_output_content_and_success(WAIT_CALL_ID)
|
||||
.is_some(),
|
||||
"the reviewed command should follow the ready-environment step"
|
||||
);
|
||||
let guardian_request = requests
|
||||
.iter()
|
||||
.find(|request| {
|
||||
request.body_json()["client_metadata"]["x-openai-subagent"].as_str() == Some("guardian")
|
||||
})
|
||||
.context("expected Guardian review request")?;
|
||||
let guardian_context = guardian_request.message_input_texts("user").join("\n");
|
||||
for expected in [
|
||||
"<environment id=\"remote\" primary=\"true\">".to_string(),
|
||||
format!("<cwd>{}</cwd>", remote_cwd.display()),
|
||||
format!("- path `{}`", remote_denied_path.display()),
|
||||
] {
|
||||
assert!(
|
||||
guardian_context.contains(&expected),
|
||||
"Guardian omitted `{expected}` from the ready environment context: {guardian_context}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!guardian_context.contains(&format!("- path `{}`", local_denied_path.display())),
|
||||
"Guardian used the stale local environment's denied-read policy: {guardian_context}"
|
||||
);
|
||||
let rejection = requests
|
||||
.iter()
|
||||
.find_map(|request| request.function_call_output_text(EXEC_CALL_ID))
|
||||
.context("Guardian denial should be returned to the parent model")?;
|
||||
assert!(rejection.contains(DENIAL_RATIONALE));
|
||||
|
||||
shutdown_tx
|
||||
.send(())
|
||||
.expect("stop remote environment server");
|
||||
exec_server.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn deferred_executor_loads_agents_md_when_environment_becomes_ready() -> Result<()> {
|
||||
const AGENTS_CONTENT: &str = "REMOTE_AGENTS_INSTRUCTIONS";
|
||||
|
||||
Reference in New Issue
Block a user