mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
## What changed - Let approval extensions choose between cached approval, synchronous review, and a user prompt for tool and permission requests, while core enforces mandatory Guardian and fresh-review requirements. - Carry an optional `review_reason` on Guardian assessment events and use it to trigger strict-review notifications, retaining the fallback for older events. - Require synchronous review when cached evidence has an unusable encrypted parent compaction. ## Testing Add regression coverage for manual prompts and cached approvals with non-UTF-8 working directories. Update approval contributors in existing tests to use the decision API. GitOrigin-RevId: a4e66416070bdaa881daa41429c49021629f44c1
178 lines
5.8 KiB
Rust
178 lines
5.8 KiB
Rust
use super::*;
|
|
use crate::protocol::thread_history::build_turns_from_rollout_items;
|
|
use codex_protocol::protocol::EventMsg;
|
|
use codex_protocol::protocol::ExecCommandSource;
|
|
use codex_protocol::protocol::GuardianAssessmentStatus;
|
|
use codex_protocol::protocol::TurnStartedEvent;
|
|
use codex_rollout::RolloutItem;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn read_command_actions_preserve_native_and_foreign_paths() {
|
|
let api_key = "sk-abcdefghijklmnopqrstuvwxyz123456";
|
|
for (cwd_uri, relative_path, expected_path) in [
|
|
(
|
|
"file:///home/alice/repo",
|
|
"src/main.rs",
|
|
"/home/alice/repo/src/main.rs",
|
|
),
|
|
(
|
|
"file:///C:/Users/Alice%20Smith/repo",
|
|
r"src\main.rs",
|
|
r"C:\Users\Alice Smith\repo\src\main.rs",
|
|
),
|
|
(
|
|
"file:///C:/Users/Alice%20Smith/repo",
|
|
r"C:src\main.rs",
|
|
r"C:\Users\Alice Smith\repo\src\main.rs",
|
|
),
|
|
(
|
|
"file://server/share/repo",
|
|
r"src\main.rs",
|
|
r"\\server\share\repo\src\main.rs",
|
|
),
|
|
] {
|
|
let cwd = PathUri::parse(cwd_uri).expect("valid cross-platform cwd");
|
|
let command = format!("cat {relative_path}");
|
|
let parsed_cmd = vec![
|
|
ParsedCommand::Read {
|
|
cmd: command.clone(),
|
|
name: "main.rs".to_string(),
|
|
path: PathBuf::from(relative_path),
|
|
},
|
|
ParsedCommand::ListFiles {
|
|
cmd: "ls".to_string(),
|
|
path: Some("subdir".to_string()),
|
|
},
|
|
ParsedCommand::Search {
|
|
cmd: format!("rg {api_key}"),
|
|
query: Some(api_key.to_string()),
|
|
path: Some("src".to_string()),
|
|
},
|
|
ParsedCommand::Search {
|
|
cmd: "rg needle".to_string(),
|
|
query: Some("needle".to_string()),
|
|
path: Some("src".to_string()),
|
|
},
|
|
];
|
|
|
|
assert_eq!(
|
|
serde_json::to_value(command_actions_for_path_uri(&parsed_cmd, &cwd))
|
|
.expect("command actions should serialize"),
|
|
json!([
|
|
{
|
|
"type": "read",
|
|
"command": command,
|
|
"name": "main.rs",
|
|
"path": expected_path,
|
|
},
|
|
{
|
|
"type": "listFiles",
|
|
"command": "ls",
|
|
"path": "subdir",
|
|
},
|
|
{
|
|
"type": "search",
|
|
"command": "rg [REDACTED_SECRET]",
|
|
"query": "[REDACTED_SECRET]",
|
|
"path": "src",
|
|
},
|
|
{
|
|
"type": "search",
|
|
"command": "rg needle",
|
|
"query": "needle",
|
|
"path": "src",
|
|
},
|
|
]),
|
|
"resolving command actions against {cwd_uri}",
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn guardian_stdin_reviews_preserve_parent_command_history() {
|
|
let cwd = PathUri::parse("file:///home/alice/repo").expect("valid cwd URI");
|
|
let begin = ExecCommandBeginEvent {
|
|
call_id: "terminal-command".into(),
|
|
plugin_id: None,
|
|
script_path: None,
|
|
process_id: Some("42".into()),
|
|
turn_id: "turn-1".into(),
|
|
started_at_ms: 1_000,
|
|
command: vec!["cat".into()],
|
|
cwd: cwd.clone(),
|
|
parsed_cmd: vec![ParsedCommand::Unknown { cmd: "cat".into() }],
|
|
source: ExecCommandSource::UnifiedExecStartup,
|
|
interaction_input: None,
|
|
};
|
|
let start_turn = |turn_id: &str| {
|
|
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
|
turn_id: turn_id.into(),
|
|
trace_id: None,
|
|
started_at: None,
|
|
model_context_window: None,
|
|
collaboration_mode_kind: Default::default(),
|
|
}))
|
|
};
|
|
let mut items = vec![
|
|
start_turn("turn-1"),
|
|
RolloutItem::EventMsg(EventMsg::ExecCommandBegin(begin.clone())),
|
|
start_turn("turn-2"),
|
|
];
|
|
let expected = build_turns_from_rollout_items(&items);
|
|
assert_eq!(expected.len(), 2);
|
|
assert_eq!(
|
|
expected[0].items,
|
|
vec![build_command_execution_begin_item(&begin)],
|
|
);
|
|
assert!(expected[1].items.is_empty());
|
|
|
|
let assessment = GuardianAssessmentEvent {
|
|
review_reason: None,
|
|
id: "review-stdin".into(),
|
|
target_item_id: Some("terminal-command".into()),
|
|
plugin_id: None,
|
|
script_path: None,
|
|
turn_id: "turn-2".into(),
|
|
started_at_ms: 2_000,
|
|
completed_at_ms: None,
|
|
status: GuardianAssessmentStatus::InProgress,
|
|
risk_level: None,
|
|
user_authorization: None,
|
|
rationale: None,
|
|
decision_source: None,
|
|
action: GuardianAssessmentAction::WriteStdin {
|
|
approval_id: "stdin-approval".into(),
|
|
process_id: "42".into(),
|
|
stdin: "yes\n".into(),
|
|
cwd,
|
|
},
|
|
};
|
|
|
|
for turn_id in ["turn-1", "turn-2"] {
|
|
for status in [
|
|
GuardianAssessmentStatus::InProgress,
|
|
GuardianAssessmentStatus::Approved,
|
|
GuardianAssessmentStatus::Denied,
|
|
GuardianAssessmentStatus::TimedOut,
|
|
GuardianAssessmentStatus::Aborted,
|
|
] {
|
|
items.push(RolloutItem::EventMsg(EventMsg::GuardianAssessment(
|
|
GuardianAssessmentEvent {
|
|
review_reason: None,
|
|
turn_id: turn_id.into(),
|
|
status,
|
|
..assessment.clone()
|
|
},
|
|
)));
|
|
assert_eq!(
|
|
build_turns_from_rollout_items(&items),
|
|
expected,
|
|
"stdin review {status:?} in {turn_id} must preserve command history",
|
|
);
|
|
items.pop();
|
|
}
|
|
}
|
|
}
|