Add plumbing to approve stored Auto-Review denials (#18955)

## Summary

This adds the structural plumbing needed for an app-server client to
approve a previously denied Guardian review and carry that approval
context into the next model turn.

This PR does not add the actual `/auto-review-denials` tool 

## What Changed

- Added app-server v2 RPC `thread/approveGuardianDeniedAction`.
- Added generated JSON schema and TypeScript fixtures for
`ThreadApproveGuardianDeniedAction*`.
- Added core `Op::ApproveGuardianDeniedAction`.
- Added a core handler that validates the event is a denied Guardian
assessment and injects a developer message containing the stored denial
event JSON.
- Queues the approval context for the next turn if there is no active
turn yet.
- Added the TUI app-server bridge so `Op::ApproveGuardianDeniedAction {
event }` is routed to the app-server request.

## What This Does Not Do

- Does not add `/auto-review-denials`.
- Does not add chat widget recent-denial state.
- Does not add popup/list UI.
- Does not add a product-facing denial lookup/store.
- Does not change where Guardian denials are originally emitted or
persisted.

## Verification

- `cargo test -p codex-tui thread_approve_guardian_denied_action`
This commit is contained in:
Won Park
2026-04-22 10:38:19 -07:00
committed by GitHub
Unverified
parent 78593d72ea
commit 11e5af53c4
17 changed files with 328 additions and 2 deletions
@@ -133,6 +133,8 @@ use codex_app_server_protocol::SkillsListParams;
use codex_app_server_protocol::SkillsListResponse;
use codex_app_server_protocol::SortDirection;
use codex_app_server_protocol::Thread;
use codex_app_server_protocol::ThreadApproveGuardianDeniedActionParams;
use codex_app_server_protocol::ThreadApproveGuardianDeniedActionResponse;
use codex_app_server_protocol::ThreadArchiveParams;
use codex_app_server_protocol::ThreadArchiveResponse;
use codex_app_server_protocol::ThreadArchivedNotification;
@@ -954,6 +956,13 @@ impl CodexMessageProcessor {
self.thread_shell_command(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::ThreadApproveGuardianDeniedAction { request_id, params } => {
self.thread_approve_guardian_denied_action(
to_connection_request_id(request_id),
params,
)
.await;
}
ClientRequest::SkillsList { request_id, params } => {
self.skills_list(to_connection_request_id(request_id), params)
.await;
@@ -3714,6 +3723,59 @@ impl CodexMessageProcessor {
}
}
async fn thread_approve_guardian_denied_action(
&self,
request_id: ConnectionRequestId,
params: ThreadApproveGuardianDeniedActionParams,
) {
let ThreadApproveGuardianDeniedActionParams { thread_id, event } = params;
let event = match serde_json::from_value(event) {
Ok(event) => event,
Err(err) => {
self.outgoing
.send_error(
request_id,
JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: format!("invalid Guardian denial event: {err}"),
data: None,
},
)
.await;
return;
}
};
let (_, thread) = match self.load_thread(&thread_id).await {
Ok(v) => v,
Err(error) => {
self.outgoing.send_error(request_id, error).await;
return;
}
};
match self
.submit_core_op(
&request_id,
thread.as_ref(),
Op::ApproveGuardianDeniedAction { event },
)
.await
{
Ok(_) => {
self.outgoing
.send_response(request_id, ThreadApproveGuardianDeniedActionResponse {})
.await;
}
Err(err) => {
self.send_internal_error(
request_id,
format!("failed to approve Guardian denial: {err}"),
)
.await;
}
}
}
async fn thread_list(&self, request_id: ConnectionRequestId, params: ThreadListParams) {
let ThreadListParams {
cursor,