Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1424,6 +1424,16 @@ client_request_definitions! {
serialization: thread_id(params.thread_id),
response: v2::ReviewStartResponse,
},
ReviewPublisherStatusRead => "review/publisher/status/read" {
params: v2::ReviewPublisherStatusReadParams,
serialization: global_shared_read("review-publisher"),
response: v2::ReviewPublisherStatusReadResponse,
},
ReviewPublisherReplay => "review/publisher/replay" {
params: v2::ReviewPublisherReplayParams,
serialization: global("review-publisher"),
response: v2::ReviewPublisherReplayResponse,
},

ModelList => "model/list" {
params: v2::ModelListParams,
Expand Down
115 changes: 115 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/review.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ pub struct ReviewStartParams {
#[serde(default)]
#[ts(optional = nullable)]
pub delivery: Option<ReviewDelivery>,

/// Exact pull-request candidate metadata for authenticated check publishing.
/// Repository origin and implementer identity are deliberately absent: the
/// app-server derives them from the clean local Git checkout.
#[serde(default)]
#[ts(optional = nullable)]
pub publisher_context: Option<ReviewPublisherContext>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
Expand All @@ -35,6 +42,114 @@ pub struct ReviewStartResponse {
/// For inline reviews, this is the original thread id.
/// For detached reviews, this is the id of the new review thread.
pub review_thread_id: String,
/// Stable durable review run id when `publisherContext` was supplied.
pub review_run_id: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ReviewPublisherContext {
pub pull_request_number: u64,
pub base_ref: String,
pub reviewed_base_sha: String,
pub head_sha: String,
pub acceptance_scope_id: String,
pub acceptance_scope_sha256: String,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ReviewPublisherStatusReadParams {
pub review_run_id: String,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ReviewPublisherStatusReadResponse {
pub run: Option<ReviewPublisherRun>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ReviewPublisherReplayParams {
pub event_id: String,
pub payload_sha256: String,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ReviewPublisherReplayResponse {
pub event: Option<ReviewPublisherOutboxEvent>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ReviewPublisherRun {
pub review_run_id: String,
pub envelope_sha256: String,
pub status: ReviewPublisherRunStatus,
pub verdict: Option<ReviewPublisherVerdict>,
pub created_at: i64,
pub completed_at: Option<i64>,
pub events: Vec<ReviewPublisherOutboxEvent>,
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase", export_to = "v2/")]
pub enum ReviewPublisherRunStatus {
Started,
Completed,
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[ts(rename_all = "SCREAMING_SNAKE_CASE", export_to = "v2/")]
pub enum ReviewPublisherVerdict {
Go,
NoGo,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ReviewPublisherOutboxEvent {
pub event_id: String,
pub event_kind: ReviewPublisherEventKind,
pub sequence: u8,
pub status: ReviewPublisherEventStatus,
pub payload_sha256: String,
pub attempt_count: u32,
pub next_attempt_at: i64,
pub lease_expires_at: Option<i64>,
pub receipt_id: Option<String>,
pub last_error_code: Option<String>,
pub created_at: i64,
pub delivered_at: Option<i64>,
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase", export_to = "v2/")]
pub enum ReviewPublisherEventKind {
Started,
Completed,
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase", export_to = "v2/")]
pub enum ReviewPublisherEventStatus {
Pending,
InFlight,
Delivered,
DeadLetter,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "app-server",
crate_name = "codex_app_server",
deps_extra = ["@crates//:reqwest"],
integration_test_timeout = "long",
test_shard_counts = {
# Note app-server-all-test has a large number of integration tests, so
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1631,6 +1631,10 @@ Example request/response:

For a detached review, use `"delivery": "detached"`. The response is the same shape, but `reviewThreadId` will be the id of the new review thread (different from the original `threadId`). The server also emits a `thread/started` notification for that new thread before streaming the review turn.

An authenticated publisher can add `publisherContext` to a `baseBranch` review. This path is fail-closed: `baseRef` must be the same full Git ref used by the target, the worktree must be clean, `reviewedBaseSha` and `headSha` must resolve exactly, `git merge-tree --write-tree` must produce a clean result, and the head commit must carry exactly one `Agent:` trailer. The server derives the canonical origin and implementer from Git, persists an immutable `codewith-review-envelope-v1` start event before starting the turn, and returns its stable `reviewRunId`. It publishes the terminal `GO` or `NO_GO` event only from structured review output; missing output, unknown correctness, malformed priorities, or P0/P1 findings all map to `NO_GO`.

The owner-only outbox dispatcher is enabled only when `CODEWITH_REVIEW_PUBLISHER_URL` names an HTTPS endpoint (loopback HTTP is allowed for local development) and `CODEWITH_REVIEW_PUBLISHER_CREDENTIAL_ENV` names the environment variable holding its bearer credential. The credential itself is never accepted in RPC payloads or persisted. Delivery is ordered start-before-terminal, leases in-flight work, treats HTTP 409 as an idempotent receipt, retries timeouts/429/5xx, and dead-letters permanent failures. Inspect a run with `review/publisher/status/read`; replay only an exact immutable payload by passing both `eventId` and `payloadSha256` to `review/publisher/replay`.

Codewith streams the usual `turn/started` notification followed by an `item/started`
with an `enteredReviewMode` item so clients can show progress:

Expand Down
21 changes: 21 additions & 0 deletions codex-rs/app-server/src/bespoke_event_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,27 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
}
EventMsg::ExitedReviewMode(review_event) => {
if let Some(envelope) = review_event.review_envelope.as_ref() {
if let Some(state_db) = conversation.state_db() {
let review_run_id = codex_state::review_run_id_from_envelope_sha256(
envelope.envelope_sha256.as_str(),
);
if let Err(err) = state_db
.review_publisher()
.complete_review_run(codex_state::ReviewPublisherCompleteParams {
review_run_id,
envelope_sha256: envelope.envelope_sha256.clone(),
review_output: review_event.review_output.clone(),
terminal_reason_override: None,
})
.await
{
warn!("failed to persist review publisher terminal event: {err}");
}
} else {
warn!("review publisher terminal event has no durable state runtime");
}
}
let review = match review_event.review_output {
Some(output) => render_review_output_text(&output),
None => REVIEW_FALLBACK_MESSAGE.to_string(),
Expand Down
20 changes: 20 additions & 0 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ use crate::request_processors::PluginRequestProcessor;
use crate::request_processors::ProcessExecRequestProcessor;
use crate::request_processors::RemoteControlRequestProcessor;
use crate::request_processors::RemoteDispatchRequestProcessor;
use crate::request_processors::ReviewPublisherDispatcherRuntime;
use crate::request_processors::ReviewPublisherRequestProcessor;
use crate::request_processors::SearchRequestProcessor;
use crate::request_processors::ThreadGoalRequestProcessor;
use crate::request_processors::ThreadMailboxDispatcherRuntime;
Expand Down Expand Up @@ -201,6 +203,8 @@ pub(crate) struct MessageProcessor {
plugin_processor: PluginRequestProcessor,
remote_control_processor: RemoteControlRequestProcessor,
remote_dispatch_processor: RemoteDispatchRequestProcessor,
review_publisher_dispatcher_runtime: ReviewPublisherDispatcherRuntime,
review_publisher_processor: ReviewPublisherRequestProcessor,
search_processor: SearchRequestProcessor,
thread_goal_processor: ThreadGoalRequestProcessor,
thread_mailbox_dispatcher_runtime: Option<ThreadMailboxDispatcherRuntime>,
Expand Down Expand Up @@ -504,6 +508,10 @@ impl MessageProcessor {
let machine_registry_processor = MachineRegistryRequestProcessor::new(state_db.clone());
let remote_control_processor = RemoteControlRequestProcessor::new(remote_control_handle);
let remote_dispatch_processor = RemoteDispatchRequestProcessor::new(state_db.clone());
let review_publisher_dispatcher_runtime =
ReviewPublisherDispatcherRuntime::new(state_db.clone());
review_publisher_dispatcher_runtime.start();
let review_publisher_processor = ReviewPublisherRequestProcessor::new(state_db.clone());
let search_processor = SearchRequestProcessor::new(outgoing.clone());
let thread_goal_processor = ThreadGoalRequestProcessor::new(
Arc::clone(&thread_manager),
Expand Down Expand Up @@ -677,6 +685,8 @@ impl MessageProcessor {
plugin_processor,
remote_control_processor,
remote_dispatch_processor,
review_publisher_dispatcher_runtime,
review_publisher_processor,
search_processor,
thread_goal_processor,
thread_mailbox_dispatcher_runtime,
Expand Down Expand Up @@ -709,6 +719,7 @@ impl MessageProcessor {
if let Some(runtime) = self.thread_mailbox_dispatcher_runtime.as_ref() {
runtime.shutdown();
}
self.review_publisher_dispatcher_runtime.shutdown();
self.thread_monitor_runtime.shutdown();
self.thread_schedule_runtime.shutdown();
}
Expand Down Expand Up @@ -894,6 +905,9 @@ impl MessageProcessor {
if let Some(runtime) = self.thread_mailbox_dispatcher_runtime.as_ref() {
runtime.drain_background_tasks().await;
}
self.review_publisher_dispatcher_runtime
.drain_background_tasks()
.await;
self.thread_monitor_runtime.drain_background_tasks().await;
self.thread_schedule_runtime.drain_background_tasks().await;
self.thread_processor.drain_background_tasks().await;
Expand Down Expand Up @@ -1874,6 +1888,12 @@ impl MessageProcessor {
ClientRequest::ReviewStart { params, .. } => {
self.turn_processor.review_start(&request_id, params).await
}
ClientRequest::ReviewPublisherStatusRead { params, .. } => {
self.review_publisher_processor.status_read(params).await
}
ClientRequest::ReviewPublisherReplay { params, .. } => {
self.review_publisher_processor.replay(params).await
}
ClientRequest::McpServerOauthLogin { params, .. } => {
self.mcp_processor.mcp_server_oauth_login(params).await
}
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/app-server/src/request_processors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,7 @@ mod plugins;
mod process_exec_processor;
mod remote_control_processor;
mod remote_dispatch_processor;
mod review_publisher;
mod search;
mod sqlite_retry;
mod thread_external_agent_processor;
Expand Down Expand Up @@ -715,6 +716,9 @@ pub(crate) use plugins::PluginRequestProcessor;
pub(crate) use process_exec_processor::ProcessExecRequestProcessor;
pub(crate) use remote_control_processor::RemoteControlRequestProcessor;
pub(crate) use remote_dispatch_processor::RemoteDispatchRequestProcessor;
pub(crate) use review_publisher::ReviewPublisherDispatcherRuntime;
pub(crate) use review_publisher::ReviewPublisherRequestProcessor;
pub(crate) use review_publisher::build_review_envelope;
pub(crate) use search::SearchRequestProcessor;
pub(crate) use thread_goal_processor::ThreadGoalRequestProcessor;
pub(crate) use thread_mailbox_dispatcher_runtime::ThreadMailboxDispatcherRuntime;
Expand Down
Loading
Loading