Skip to content
Merged
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
129 changes: 127 additions & 2 deletions apps/decodex/src/radar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const DEFAULT_TAG_PREFIX: &str = "rust-v";
const RELEASE_DELTA_SCHEMA: &str = "release_delta/v1";
const SCHEMA_VERSION: i64 = 2;
const SIGNAL_SCHEMA: &str = "signal_entry/v1";
const SOCIAL_CANDIDATE_SCHEMA: &str = "social_candidate/v1";
const SOCIAL_POST_SCHEMA: &str = "social_post/v1";
const UPSTREAM_IMPACT_SCHEMA: &str = "upstream_impact/v1";
const UPSTREAM_REVIEW_QUEUE_SCHEMA: &str = "upstream_review_queue/v1";
Expand All @@ -50,6 +51,7 @@ const DEFAULT_VALIDATION_PATHS: &[&str] = &[
"artifacts/github/review-queue",
"artifacts/github/reviews",
"artifacts/github/impact",
"artifacts/github/social-candidates",
"artifacts/social/x",
"site/src/content/signals",
"site/src/content/release-deltas",
Expand All @@ -75,8 +77,14 @@ const SOCIAL_POST_WORTHINESS: &[&str] = &["block", "publish", "skip"];
const SOURCE_ITEM_KINDS: &[&str] = &["commit", "pull_request"];
const UPSTREAM_IMPACT_KINDS: &[&str] =
&["browser_observation", "changelog", "commit", "pull_request", "release", "signal"];
const UPSTREAM_REVIEW_ACTION_TYPES: &[&str] =
&["linear_followup", "none", "signal_entry", "social_post", "upstream_impact"];
const UPSTREAM_REVIEW_ACTION_TYPES: &[&str] = &[
"linear_followup",
"none",
"signal_entry",
"social_candidate",
"social_post",
"upstream_impact",
];
const UPSTREAM_REVIEW_NEXT_STEPS: &[&str] = &["ai_review_required"];
const UPSTREAM_REVIEW_PRIORITIES: &[&str] = &["critical", "high", "low", "normal"];
const UPSTREAM_SOURCE_STATES: &[&str] = &["closed", "commit_only", "merged", "open"];
Expand Down Expand Up @@ -4221,6 +4229,7 @@ fn validate_artifact(payload: &Value) -> ArtifactValidation {
Some(CONFIG_FEATURE_CATALOG_SCHEMA) => validate_config_feature_catalog(entry, &mut errors),
Some(RELEASE_DELTA_SCHEMA) => validate_release_delta(entry, &mut errors),
Some(SIGNAL_SCHEMA) => validate_signal(entry, &mut errors),
Some(SOCIAL_CANDIDATE_SCHEMA) => validate_social_candidate(entry, &mut errors),
Some(SOCIAL_POST_SCHEMA) => validate_social_post(entry, &mut errors),
Some(UPSTREAM_IMPACT_SCHEMA) => validate_upstream_impact(entry, &mut errors),
Some(UPSTREAM_REVIEW_QUEUE_SCHEMA) => validate_upstream_review_queue(entry, &mut errors),
Expand Down Expand Up @@ -5137,6 +5146,76 @@ fn validate_upstream_impact_source_refs(refs: Option<&Value>, errors: &mut Vec<S
}
}

fn validate_social_candidate(entry: &Map<String, Value>, errors: &mut Vec<String>) {
for field in ["slug", "repo", "audience"] {
if !is_non_empty_string(entry.get(field)) {
errors.push(format!("{field} must be a non-empty string"));
}
}

if string_field(entry, "repo").is_some_and(|repo| !repo.contains('/')) {
errors.push("repo must be owner/name".into());
}
if string_field(entry, "channel") != Some("x") {
errors.push("channel must be x".into());
}
if string_field(entry, "target_account") != Some("decodexspace") {
errors.push("target_account must be decodexspace".into());
}
if !matches_one_of(entry.get("mode"), SOCIAL_POST_MODES) {
errors.push(format!("mode must be one of {}", choices(SOCIAL_POST_MODES)));
}
if !matches_one_of(entry.get("priority"), SOCIAL_POST_PRIORITIES) {
errors.push(format!("priority must be one of {}", choices(SOCIAL_POST_PRIORITIES)));
}

validate_social_post_text(entry.get("candidate_text"), errors);
validate_social_candidate_source_refs(entry.get("source_refs"), errors);
validate_non_empty_string_list(entry.get("evidence_notes"), "evidence_notes", errors);
validate_social_post_claims(entry.get("claims"), errors);
validate_social_candidate_decision(entry.get("decision"), errors);

for field in ["caveats", "next_steps"] {
validate_optional_string_list(entry.get(field), field, errors);
}
}

fn validate_social_candidate_source_refs(refs: Option<&Value>, errors: &mut Vec<String>) {
let Some(refs) = refs.and_then(Value::as_object) else {
errors.push("source_refs must be an object".into());

return;
};
let has_refs = ["upstream_reviews", "upstream_impacts", "signals", "release_deltas", "urls"]
.iter()
.any(|field| refs.get(*field).is_some_and(|value| !is_empty_or_missing_array(Some(value))));

if !has_refs {
errors.push(
"source_refs must include upstream_reviews, upstream_impacts, signals, release_deltas, or urls"
.into(),
);
}
}

fn validate_social_candidate_decision(decision: Option<&Value>, errors: &mut Vec<String>) {
let Some(decision) = decision.and_then(Value::as_object) else {
errors.push("decision must be an object".into());

return;
};

if !matches_one_of(decision.get("worthiness"), &["defer", "publish", "skip"]) {
errors.push("decision.worthiness must be one of ['defer', 'publish', 'skip']".into());
}

for field in ["reason", "idempotency_key"] {
if !is_non_empty_string(decision.get(field)) {
errors.push(format!("decision.{field} must be a non-empty string"));
}
}
}

fn validate_social_post(entry: &Map<String, Value>, errors: &mut Vec<String>) {
for field in ["slug", "audience"] {
if !is_non_empty_string(entry.get(field)) {
Expand Down Expand Up @@ -5503,6 +5582,7 @@ fn known_schemas() -> String {
CONFIG_FEATURE_CATALOG_SCHEMA,
RELEASE_DELTA_SCHEMA,
SIGNAL_SCHEMA,
SOCIAL_CANDIDATE_SCHEMA,
SOCIAL_POST_SCHEMA,
UPSTREAM_IMPACT_SCHEMA,
UPSTREAM_REVIEW_QUEUE_SCHEMA,
Expand Down Expand Up @@ -5746,6 +5826,17 @@ mod tests {
assert_errors(&review, ["next_actions[0].type must be one of"]);
}

#[test]
fn accepts_valid_social_candidate_and_rejects_missing_refs() {
let mut candidate = valid_social_candidate();

assert_errors(&candidate, []);

candidate["source_refs"] = serde_json::json!({});

assert_errors(&candidate, ["source_refs must include upstream_reviews"]);
}

#[test]
fn accepts_valid_upstream_impact_and_rejects_bad_angle() {
let mut impact = valid_upstream_impact();
Expand Down Expand Up @@ -6449,6 +6540,40 @@ mod tests {
})
}

fn valid_social_candidate() -> Value {
serde_json::json!({
"schema": "social_candidate/v1",
"slug": "openai-codex-pr-22414",
"repo": "openai/codex",
"channel": "x",
"target_account": "decodexspace",
"mode": "operator_impact",
"priority": "normal",
"audience": "Codex operators",
"candidate_text": [
"Remote Codex can now use Unix socket endpoints. Source: https://github.com/openai/codex/pull/22414"
],
"source_refs": {
"upstream_reviews": ["artifacts/github/reviews/openai-codex-pr-22414.review.json"],
"upstream_impacts": ["artifacts/github/impact/openai-codex-pr-22414.json"],
"urls": ["https://github.com/openai/codex/pull/22414"]
},
"evidence_notes": ["PR #22414 changes remote endpoint handling."],
"claims": [
{
"text": "Remote Codex can use Unix socket endpoints.",
"evidence": "https://github.com/openai/codex/pull/22414",
"confidence": "confirmed"
}
],
"decision": {
"worthiness": "publish",
"reason": "The source-backed review has a clear operator impact angle.",
"idempotency_key": "x:decodexspace:openai-codex-pr-22414:operator_impact"
}
})
}

fn valid_social_post() -> Value {
serde_json::json!({
"schema": "social_post/v1",
Expand Down
1 change: 1 addition & 0 deletions artifacts/github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This directory stores checked-in GitHub signal pipeline artifacts.
- `bundles/` holds normalized `github_change_bundle/v1` inputs.
- `analysis/` holds reviewed Codex editorial analysis drafts.
- `impact/` holds optional `upstream_impact/v1` classifications.
- `social-candidates/` holds optional `social_candidate/v1` Publisher handoffs.

`bundles/` and `analysis/` are hot raw artifact directories. Keep raw entries in Git for
at most 21 days, then move cold batches to dedicated `radar-archive-*` GitHub Release
Expand Down
79 changes: 79 additions & 0 deletions artifacts/github/bundles/openai-codex-pr-25469.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"analysis_mode": "pr_first",
"commits": [
{
"author": "dhruvgupta-oai",
"committed_at": "2026-06-01T00:41:19Z",
"message": "Add app server account session protocol",
"sha": "cc0c295e8d848ddfe2f3205dbce32bfc4f4c04b6",
"url": "https://github.com/openai/codex/commit/cc0c295e8d848ddfe2f3205dbce32bfc4f4c04b6"
},
{
"author": "dhruvgupta-oai",
"committed_at": "2026-06-01T02:13:39Z",
"message": "Fix account session protocol layering",
"sha": "5327fc53dc007e3b35f092145d616f0339b18f94",
"url": "https://github.com/openai/codex/commit/5327fc53dc007e3b35f092145d616f0339b18f94"
},
{
"author": "dhruvgupta-oai",
"committed_at": "2026-06-02T09:02:29Z",
"message": "Remove unused account session metadata",
"sha": "3ad459228152dd5a64a6b68d7fbc485ede24d6a0",
"url": "https://github.com/openai/codex/commit/3ad459228152dd5a64a6b68d7fbc485ede24d6a0"
}
],
"default_branch": "main",
"docs_refs": [],
"examples_refs": [],
"extracted_flags": [
"GET"
],
"files": [
{
"additions": 72,
"deletions": 0,
"patch_excerpt": "@@ -138,6 +138,78 @@ pub struct CancelLoginAccountResponse {\n pub status: CancelLoginAccountStatus,\n }\n \n+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]\n+#[serde(rename_all = \"camelCase\")]\n+#[ts(export_to = \"v2/\")]\n+pub struct AccountSessionsAddParams {\n+ #[serde(default, skip_serializing_if = \"std::ops::Not::not\")]\n+ pub switch_to_added_account: bool,\n+}\n+\n+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]\n+#[serde(rename_all = \"camelCase\")]\n+#[ts(export_to = \"v2/\")]\n+pub struct AccountSessionsListParams {\n+ #[serde(default, skip_serializing_if = \"std::ops::Not::not\")]\n+ pub refresh_workspace_metadata: bool,\n+}\n+\n+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]\n+#[serde(rename_all = \"camelCase\")]\n+#[ts(export_to = \"v2/\")]\n+pub struct AccountSessionsLogoutParams {\n+ pub session_...",
"path": "codex-rs/app-server-protocol/src/protocol/v2/account.rs",
"status": "modified"
},
{
"additions": 11,
"deletions": 0,
"patch_excerpt": "@@ -1,3 +1,4 @@\n+use crate::types::AccountsCheckResponse;\n use crate::types::CodeTaskDetailsResponse;\n use crate::types::ConfigBundleResponse;\n use crate::types::ConfigFileResponse;\n@@ -303,6 +304,16 @@ impl Client {\n Ok(Self::rate_limit_snapshots_from_payload(payload))\n }\n \n+ pub async fn get_accounts_check(&self) -> Result<AccountsCheckResponse> {\n+ let url = match self.path_style {\n+ PathStyle::CodexApi => format!(\"{}/api/codex/accounts/check\", self.base_url),\n+ PathStyle::ChatGptApi => format!(\"{}/wham/accounts/check\", self.base_url),\n+ };\n+ let req = self.http.get(&url).headers(self.headers());\n+ let (body, ct) = self.exec_request(req, \"GET\", &url).await?;\n+ self.decode_json(&url, &ct, &body)\n+ }\n+\n pub async fn send_add_credits_nudge_email(\n &self,\n credit_type: AddCreditsNudgeCreditType,",
"path": "codex-rs/backend-client/src/client.rs",
"status": "modified"
},
{
"additions": 2,
"deletions": 0,
"patch_excerpt": "@@ -4,6 +4,8 @@ pub(crate) mod types;\n pub use client::AddCreditsNudgeCreditType;\n pub use client::Client;\n pub use client::RequestError;\n+pub use types::AccountEntry;\n+pub use types::AccountsCheckResponse;\n pub use types::CodeTaskDetailsResponse;\n pub use types::CodeTaskDetailsResponseExt;\n pub use types::ConfigBundleResponse;",
"path": "codex-rs/backend-client/src/lib.rs",
"status": "modified"
},
{
"additions": 87,
"deletions": 0,
"patch_excerpt": "@@ -18,6 +18,93 @@ use serde::de::Deserializer;\n use serde_json::Value;\n use std::collections::HashMap;\n \n+#[derive(Clone, Debug)]\n+pub struct AccountsCheckResponse {\n+ pub accounts: Vec<AccountEntry>,\n+ pub account_ordering: Vec<String>,\n+ pub default_account_id: Option<String>,\n+}\n+\n+#[derive(Clone, Debug, Deserialize)]\n+pub struct AccountEntry {\n+ pub id: String,\n+ #[serde(default)]\n+ pub name: Option<String>,\n+ #[serde(default)]\n+ pub profile_picture_url: Option<String>,\n+ #[serde(default)]\n+ pub structure: String,\n+}\n+\n+#[derive(Deserialize)]\n+struct RawAccountsCheckResponse {\n+ #[serde(default)]\n+ accounts: RawAccounts,\n+ #[serde(default)]\n+ account_ordering: Vec<String>,\n+ #[serde(default)]\n+ default_account_id: Option<String>,\n+}\n+\n+#[derive(Deserialize)]\n+#[serde(untagged)]\n+enum RawAccounts {\n+ List(Vec<AccountEntry>),\n+ Map...",
"path": "codex-rs/backend-client/src/types.rs",
"status": "modified"
}
],
"linked_issues": [
"openai/codex#25383"
],
"notes": [
"Built from GitHub pull-request, commits, files, and repo endpoints."
],
"primary_pr": {
"body": "## Summary\n\nAdds the app-server v2 `accountSession/*` protocol used by the Desktop profile switcher and the backend account metadata client needed to populate workspace choices.\n\nThis is the protocol layer only. The app-server lifecycle and consolidated saved-session storage are split into a follow-up PR.\n\n## Rust Stack\n\n1. This PR\n2. [openai/codex#25383](https://github.com/openai/codex/pull/25383) adds app-server session lifecycle behavior and consolidated saved-session storage.\n\n## Validation\n\n- Generated app-server schema fixtures are included from the existing generation flow in the lifecycle PR where the routes are registered.\n- Did not run tests per requested scope.\n",
"labels": [],
"merged_at": "2026-06-03T19:55:12Z",
"number": 25469,
"state": "merged",
"title": "[profile-switcher][rust] -- [1/2] Add app-server account session protocol",
"url": "https://github.com/openai/codex/pull/25469"
},
"repo": "openai/codex",
"schema": "github_change_bundle/v1"
}
Loading