mirror of
https://github.com/openai/codex.git
synced 2026-04-29 08:56:38 +00:00
[app-server] feat: v2 apply_patch approval flow (#6760)
This PR adds the API V2 version of the apply_patch approval flow, which centers around `ThreadItem::FileChange`. This PR wires the new RPC (`item/fileChange/requestApproval`, V2 only) and related events (`item/started`, `item/completed` for `ThreadItem::FileChange`, which are emitted in both V1 and V2) through the app-server protocol. The new approval RPC is only sent when the user initiates a turn with the new `turn/start` API so we don't break backwards compatibility with VSCE. Similar to https://github.com/openai/codex/pull/6758, the approach I took was to make as few changes to the Codex core as possible, leveraging existing `EventMsg` core events, and translating those in app-server. I did have to add a few additional fields to `EventMsg::PatchApplyBegin` and `EventMsg::PatchApplyEnd`, but those were fairly lightweight. However, the `EventMsg`s emitted by core are the following: ``` 1) Auto-approved (no request for approval) - EventMsg::PatchApplyBegin - EventMsg::PatchApplyEnd 2) Approved by user - EventMsg::ApplyPatchApprovalRequest - EventMsg::PatchApplyBegin - EventMsg::PatchApplyEnd 3) Declined by user - EventMsg::ApplyPatchApprovalRequest - EventMsg::PatchApplyBegin - EventMsg::PatchApplyEnd ``` For a request triggering an approval, this would result in: ``` item/fileChange/requestApproval item/started item/completed ``` which is different from the `ThreadItem::CommandExecution` flow introduced in https://github.com/openai/codex/pull/6758, which does the below and is preferable: ``` item/started item/commandExecution/requestApproval item/completed ``` To fix this, we leverage `TurnSummaryStore` on codex_message_processor to store a little bit of state, allowing us to fire `item/started` and `item/fileChange/requestApproval` whenever we receive the underlying `EventMsg::ApplyPatchApprovalRequest`, and no-oping when we receive the `EventMsg::PatchApplyBegin` later. This is much less invasive than modifying the order of EventMsg within core (I tried). The resulting payloads: ``` { "method": "item/started", "params": { "item": { "changes": [ { "diff": "Hello from Codex!\n", "kind": "add", "path": "/Users/owen/repos/codex/codex-rs/APPROVAL_DEMO.txt" } ], "id": "call_Nxnwj7B3YXigfV6Mwh03d686", "status": "inProgress", "type": "fileChange" } } } ``` ``` { "id": 0, "method": "item/fileChange/requestApproval", "params": { "grantRoot": null, "itemId": "call_Nxnwj7B3YXigfV6Mwh03d686", "reason": null, "threadId": "019a9e11-8295-7883-a283-779e06502c6f", "turnId": "1" } } ``` ``` { "id": 0, "result": { "decision": "accept" } } ``` ``` { "method": "item/completed", "params": { "item": { "changes": [ { "diff": "Hello from Codex!\n", "kind": "add", "path": "/Users/owen/repos/codex/codex-rs/APPROVAL_DEMO.txt" } ], "id": "call_Nxnwj7B3YXigfV6Mwh03d686", "status": "completed", "type": "fileChange" } } } ```
This commit is contained in:
@@ -1912,6 +1912,7 @@ fn approval_modal_patch_snapshot() {
|
||||
);
|
||||
let ev = ApplyPatchApprovalRequestEvent {
|
||||
call_id: "call-approve-patch".into(),
|
||||
turn_id: "turn-approve-patch".into(),
|
||||
changes,
|
||||
reason: Some("The model wants to apply changes".into()),
|
||||
grant_root: Some(PathBuf::from("/tmp")),
|
||||
@@ -2164,6 +2165,7 @@ fn apply_patch_events_emit_history_cells() {
|
||||
);
|
||||
let ev = ApplyPatchApprovalRequestEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
changes,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
@@ -2204,6 +2206,7 @@ fn apply_patch_events_emit_history_cells() {
|
||||
);
|
||||
let begin = PatchApplyBeginEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
auto_approved: true,
|
||||
changes: changes2,
|
||||
};
|
||||
@@ -2220,11 +2223,20 @@ fn apply_patch_events_emit_history_cells() {
|
||||
);
|
||||
|
||||
// 3) End apply success -> success cell
|
||||
let mut end_changes = HashMap::new();
|
||||
end_changes.insert(
|
||||
PathBuf::from("foo.txt"),
|
||||
FileChange::Add {
|
||||
content: "hello\n".to_string(),
|
||||
},
|
||||
);
|
||||
let end = PatchApplyEndEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
stdout: "ok\n".into(),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
changes: end_changes,
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "s1".into(),
|
||||
@@ -2252,6 +2264,7 @@ fn apply_patch_manual_approval_adjusts_header() {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
changes: proposed_changes,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
@@ -2270,6 +2283,7 @@ fn apply_patch_manual_approval_adjusts_header() {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
auto_approved: false,
|
||||
changes: apply_changes,
|
||||
}),
|
||||
@@ -2299,6 +2313,7 @@ fn apply_patch_manual_flow_snapshot() {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
changes: proposed_changes,
|
||||
reason: Some("Manual review required".into()),
|
||||
grant_root: None,
|
||||
@@ -2321,6 +2336,7 @@ fn apply_patch_manual_flow_snapshot() {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
auto_approved: false,
|
||||
changes: apply_changes,
|
||||
}),
|
||||
@@ -2348,6 +2364,7 @@ fn apply_patch_approval_sends_op_with_submission_id() {
|
||||
);
|
||||
let ev = ApplyPatchApprovalRequestEvent {
|
||||
call_id: "call-999".into(),
|
||||
turn_id: "turn-999".into(),
|
||||
changes,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
@@ -2387,6 +2404,7 @@ fn apply_patch_full_flow_integration_like() {
|
||||
id: "sub-xyz".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
call_id: "call-1".into(),
|
||||
turn_id: "turn-call-1".into(),
|
||||
changes,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
@@ -2427,17 +2445,25 @@ fn apply_patch_full_flow_integration_like() {
|
||||
id: "sub-xyz".into(),
|
||||
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
|
||||
call_id: "call-1".into(),
|
||||
turn_id: "turn-call-1".into(),
|
||||
auto_approved: false,
|
||||
changes: changes2,
|
||||
}),
|
||||
});
|
||||
let mut end_changes = HashMap::new();
|
||||
end_changes.insert(
|
||||
PathBuf::from("pkg.rs"),
|
||||
FileChange::Add { content: "".into() },
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-xyz".into(),
|
||||
msg: EventMsg::PatchApplyEnd(PatchApplyEndEvent {
|
||||
call_id: "call-1".into(),
|
||||
turn_id: "turn-call-1".into(),
|
||||
stdout: String::from("ok"),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
changes: end_changes,
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -2458,6 +2484,7 @@ fn apply_patch_untrusted_shows_approval_modal() {
|
||||
id: "sub-1".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
call_id: "call-1".into(),
|
||||
turn_id: "turn-call-1".into(),
|
||||
changes,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
@@ -2506,6 +2533,7 @@ fn apply_patch_request_shows_diff_summary() {
|
||||
id: "sub-apply".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
call_id: "call-apply".into(),
|
||||
turn_id: "turn-apply".into(),
|
||||
changes,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
|
||||
Reference in New Issue
Block a user