Skip to main content

xtask/
codex_schema.rs

1//! `cargo xtask codex-schema <subcommand>` — Rust port of the vendored
2//! `codex-app-server-client` crate's former `schema/build_combined_schema.py`,
3//! plus staleness stamping and typify-panic bisection tooling.
4//!
5//! Subcommands:
6//!   regen   Rebuild schema/protocol.schema.json + schema/methods.json from a
7//!           fresh `codex app-server generate-json-schema` dump, and stamp the
8//!           `codex` version the vendored schema was generated from.
9//!   bisect  Binary-search a fresh schema dump for the minimal definition(s)
10//!           that panic typify's schema-merge logic, the same failure mode
11//!           documented for `McpServerElicitationRequestParams` in the
12//!           crate's README.
13//!   drift   Diff the vendored schema/methods.json against a fresh (or saved)
14//!           `codex app-server generate-json-schema` dump and report added,
15//!           removed, and changed methods per section - see `drift.rs`.
16//!
17//! See `crates/shared/codex-app-server-client/README.md`'s "Regenerating the
18//! schema" section for the end-to-end workflow this drives.
19
20mod bisect;
21mod drift;
22mod merge;
23mod naming;
24mod regen;
25mod typify_probe;
26
27use anyhow::{bail, Context, Result};
28use serde_json::{Map, Value};
29use std::path::{Path, PathBuf};
30
31/// Workspace-relative path to the vendored combined protocol schema.
32pub(crate) const PROTOCOL_SCHEMA_PATH: &str =
33    "crates/shared/codex-app-server-client/schema/protocol.schema.json";
34/// Workspace-relative path to the vendored per-method manifest.
35pub(crate) const METHODS_JSON_PATH: &str =
36    "crates/shared/codex-app-server-client/schema/methods.json";
37/// Workspace-relative path to the staleness-tracking codex version stamp
38/// (see `regen::stamp_codex_version` and `build.rs`'s staleness check).
39pub(crate) const CODEX_VERSION_PATH: &str =
40    "crates/shared/codex-app-server-client/schema/CODEX_VERSION.txt";
41
42/// Filename of the master (non-v2) schema bundle emitted by
43/// `codex app-server generate-json-schema` into its output directory.
44/// Shared by `regen` and `bisect`, which both read the same dump directory -
45/// must stay byte-identical between the two or one could silently start
46/// reading a stale/mismatched file.
47pub(crate) const MASTER_BUNDLE_FILE: &str = "codex_app_server_protocol.schemas.json";
48/// Filename of the v2-only schema bundle emitted alongside the master
49/// bundle. See [`MASTER_BUNDLE_FILE`].
50pub(crate) const V2_BUNDLE_FILE: &str = "codex_app_server_protocol.v2.schemas.json";
51
52pub fn run(args: &[String]) -> Result<()> {
53    match args.first().map(String::as_str) {
54        Some("regen") => regen::run(&args[1..]),
55        Some("bisect") => bisect::run(&args[1..]),
56        Some("drift") => drift::run(&args[1..]),
57        Some("--help") | Some("-h") | Some("help") | None => {
58            print_help();
59            Ok(())
60        }
61        Some(unknown) => bail!(
62            "Unknown codex-schema subcommand: {unknown:?}\nRun `cargo xtask codex-schema --help` for usage."
63        ),
64    }
65}
66
67fn print_help() {
68    eprintln!(
69        "cargo xtask codex-schema — codex-app-server-client schema tooling
70
71USAGE:
72  cargo xtask codex-schema <subcommand> <path-to-codex-generate-json-schema-output-dir>
73
74SUBCOMMANDS:
75  regen <dir>   Rebuild schema/protocol.schema.json + schema/methods.json from
76                a `codex app-server generate-json-schema --out <dir> --experimental`
77                dump, and stamp schema/CODEX_VERSION.txt with `codex --version`.
78  bisect <dir>  Binary-search a fresh schema dump for the definition(s) that
79                panic typify's schema-merge logic (see README.md).
80  drift [--dir <dir>] [--json] [--strict]
81                Diff the vendored schema/methods.json against a fresh (or
82                --dir'd) `codex app-server generate-json-schema` dump and
83                report added/removed/changed methods per section. Missing
84                `codex` on PATH (with no --dir) is never a hard failure - it
85                prints \"skipped\" and exits 0. Run `cargo xtask codex-schema
86                drift --help` for the full flag reference.
87  help          Show this help
88
89Typical workflow after upgrading the installed `codex` CLI:
90  codex app-server generate-json-schema --out /tmp/codex-schema --experimental
91  cargo xtask codex-schema drift --dir /tmp/codex-schema   # see what changed
92  cargo xtask codex-schema regen /tmp/codex-schema
93  cargo build -p codex-app-server-client --all-targets
94  cargo clippy -p codex-app-server-client --all-targets -- -D warnings
95  cargo test -p codex-app-server-client
96
97If that panics inside typify, bisect it:
98  cargo xtask codex-schema bisect /tmp/codex-schema"
99    );
100}
101
102/// Reads and parses a JSON file, with file-path context on failure.
103pub(crate) fn read_json(path: &Path) -> Result<Value> {
104    let text = std::fs::read_to_string(path)
105        .with_context(|| format!("failed to read {}", path.display()))?;
106    serde_json::from_str(&text)
107        .with_context(|| format!("failed to parse {} as JSON", path.display()))
108}
109
110/// Parses the single positional `<gen-dir>` argument shared by the
111/// `codex-schema regen` and `codex-schema bisect` subcommands: `--help`/`-h`
112/// prints `usage` and exits 0 immediately, exactly one positional argument is
113/// accepted as the generated-schema directory, and any further positional
114/// argument is rejected. `usage` doubles as the error context when the
115/// positional argument is missing.
116pub(crate) fn parse_gen_dir(args: &[String], usage: &str) -> Result<PathBuf> {
117    let mut gen_dir = None;
118    for arg in args {
119        match arg.as_str() {
120            "--help" | "-h" => {
121                println!("{usage}");
122                std::process::exit(0);
123            }
124            other if gen_dir.is_none() => gen_dir = Some(PathBuf::from(other)),
125            other => bail!("unexpected argument: {other}"),
126        }
127    }
128    gen_dir.context(usage.to_string())
129}
130
131/// Reads the master + v2 schema bundles from `gen_dir`, merges them via
132/// `merge::build_combined`, and extracts the flat `definitions` object.
133/// Shared by `regen::regen` (which writes the combined schema out) and
134/// `bisect::bisect` (which probes the definitions with typify).
135pub(crate) fn load_combined_defs(gen_dir: &Path) -> Result<(Value, Map<String, Value>)> {
136    let master = read_json(&gen_dir.join(MASTER_BUNDLE_FILE))?;
137    let v2 = read_json(&gen_dir.join(V2_BUNDLE_FILE))?;
138
139    let combined = merge::build_combined(&master, &v2)?;
140    let combined_defs = combined
141        .get("definitions")
142        .and_then(Value::as_object)
143        .context("combined schema missing \"definitions\"")?
144        .clone();
145
146    Ok((combined, combined_defs))
147}