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
64 changes: 30 additions & 34 deletions Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# | check | composite | |
# | check-rust | command | |
# | check-site | composite | |
# | check-site-content | script | |
# | check-site-content | command | |
# | check-site-types | command | site |
# | check-vstyle | composite | |
# | check-vstyle-rust | command | |
Expand Down Expand Up @@ -59,8 +59,16 @@ dependencies = [

[tasks.check-site-content]
workspace = false
script = [
"cargo run -p decodex --bin decodex -- radar validate",
command = "cargo"
args = [
"run",
"-p",
"decodex",
"--bin",
"decodex",
"--",
"radar",
"validate",
]

[tasks.check-site-types]
Expand Down Expand Up @@ -116,11 +124,26 @@ dependencies = [

[tasks.fmt-rust]
workspace = false
script = "cargo +nightly fmt --all"
command = "rustup"
args = [
"run",
"nightly",
"cargo",
"fmt",
"--all",
]

[tasks.fmt-rust-check]
extend = "fmt-rust"
script = "cargo +nightly fmt --all -- --check"
args = [
"run",
"nightly",
"cargo",
"fmt",
"--all",
"--",
"--check",
]

[tasks.fmt-toml]
workspace = false
Expand Down Expand Up @@ -208,7 +231,7 @@ args = [
# | ----------------------- | --------- | --- |
# | test | composite | |
# | test-rust | command | |
# | test-decodex-app-stage | script | |
# | test-decodex-app-stage | command | |

[tasks.test]
clear = true
Expand All @@ -230,34 +253,7 @@ args = [

[tasks.test-decodex-app-stage]
workspace = false
script = '''
if [ "$(uname -s)" != "Darwin" ]; then
echo "Decodex App staging is macOS-only; skipping."
exit 0
fi

./apps/decodex-app/script/build_and_run.sh stage
COMMON_ROOT="$(cd "$(git rev-parse --git-common-dir)/.." && pwd)"
STAGE_DIR="${DECODEX_APP_STAGE_DIR:-$COMMON_ROOT/target/decodex-app}"
APP_PATH="$STAGE_DIR/Decodex App.app"
test -d "$APP_PATH"
test -x "$APP_PATH/Contents/MacOS/DecodexApp"
test -x "$APP_PATH/Contents/Helpers/decodex-app-helper"
test -x "$APP_PATH/Contents/Helpers/decodex"
test -f "$APP_PATH/Contents/Info.plist"
test -f "$APP_PATH/Contents/Resources/AppIcon.icns"
test -f "$APP_PATH/Contents/Resources/StatusBarIcon.png"
codesign --verify --deep --strict "$APP_PATH"
codesign --verify --strict "$APP_PATH/Contents/Helpers/decodex-app-helper"
codesign --verify --strict "$APP_PATH/Contents/Helpers/decodex"
codesign -dv --verbose=4 "$APP_PATH" 2>&1 | grep -q '^TeamIdentifier='
codesign -dv --verbose=4 "$APP_PATH" 2>&1 | grep -q 'flags=.*runtime'
plutil -extract CFBundleName raw "$APP_PATH/Contents/Info.plist" | grep -qx 'Decodex App'
plutil -extract CFBundleDisplayName raw "$APP_PATH/Contents/Info.plist" | grep -qx 'Decodex App'
plutil -extract CFBundleIconFile raw "$APP_PATH/Contents/Info.plist" | grep -qx 'AppIcon'
plutil -extract CFBundleIdentifier raw "$APP_PATH/Contents/Info.plist" | grep -qx 'space.decodex.app'
plutil -extract LSUIElement raw "$APP_PATH/Contents/Info.plist" | grep -qx 'true'
'''
command = "scripts/macos/test_decodex_app_stage.sh"

# Build
# | task | type | cwd |
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ cargo run -p decodex --bin decodex -- serve --listen-address 127.0.0.1:8912

Project-scoped commands accept `--config <PROJECT_DIR>` after the subcommand when the
operator wants to override registry-based project resolution for that command.
Use `--allow-unverified-codex` on `run`, `serve`, or `probe` only when deliberately
dogfooding a Codex build outside the locally verified app-server range; the default
guard remains fail-closed.
`decodex serve` uses hardcoded scheduler cadences: the local control-plane loop
publishes snapshots every 15 seconds, and Linear-backed queue/status scans run at
most every 5 minutes per project unless an operator or agent requests an explicit
Expand Down
56 changes: 50 additions & 6 deletions apps/decodex/src/agent/app_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,17 @@ const APP_SERVER_COMPATIBILITY_EVIDENCE: &str =
"initialize.userAgent plus successful app-server capability preflight";
const CODEX_CLI_VERSION_STABLE_0_136_0: &str = "0.136.0";
const CODEX_CLI_VERSION_BETA_0_136_0_ALPHA_2: &str = "0.136.0-alpha.2";
const SUPPORTED_CODEX_CLI_VERSION_MATCH_ORDER: &[&str] =
&[CODEX_CLI_VERSION_BETA_0_136_0_ALPHA_2, CODEX_CLI_VERSION_STABLE_0_136_0];
const SUPPORTED_CODEX_CLI_VERSION_DISPLAY_ORDER: &[&str] =
&[CODEX_CLI_VERSION_STABLE_0_136_0, CODEX_CLI_VERSION_BETA_0_136_0_ALPHA_2];
const CODEX_CLI_VERSION_DESKTOP_0_137_0_ALPHA_4: &str = "0.137.0-alpha.4";
const SUPPORTED_CODEX_CLI_VERSION_MATCH_ORDER: &[&str] = &[
CODEX_CLI_VERSION_DESKTOP_0_137_0_ALPHA_4,
CODEX_CLI_VERSION_BETA_0_136_0_ALPHA_2,
CODEX_CLI_VERSION_STABLE_0_136_0,
];
const SUPPORTED_CODEX_CLI_VERSION_DISPLAY_ORDER: &[&str] = &[
CODEX_CLI_VERSION_STABLE_0_136_0,
CODEX_CLI_VERSION_BETA_0_136_0_ALPHA_2,
CODEX_CLI_VERSION_DESKTOP_0_137_0_ALPHA_4,
];
const JSONRPC_METHOD_NOT_FOUND: i64 = -32_601;
const CHILD_BUCKET_MODEL: &str = "Model";
const WAITING_REASON_MODEL_EXECUTION: &str = "model_execution";
Expand Down Expand Up @@ -178,6 +185,20 @@ impl AppServerCapabilityPreflightReport {
});
}

fn push_warning(
&mut self,
name: &'static str,
summary: impl Into<String>,
details: BTreeMap<String, String>,
) {
self.checks.push(AppServerCapabilityPreflightCheck {
name,
status: AppServerCapabilityPreflightStatus::Warning,
summary: summary.into(),
details,
});
}

fn has_blockers(&self) -> bool {
self.checks.iter().any(|check| check.status == AppServerCapabilityPreflightStatus::Blocked)
}
Expand All @@ -196,6 +217,7 @@ impl AppServerCapabilityPreflightReport {
pub(crate) fn compatibility_status(&self) -> &'static str {
match self.compatibility_check().map(|check| check.status) {
Some(AppServerCapabilityPreflightStatus::Ok) => "supported",
Some(AppServerCapabilityPreflightStatus::Warning) => "unverified_allowed",
Some(AppServerCapabilityPreflightStatus::Blocked) => "unsupported",
None => "not_checked",
}
Expand Down Expand Up @@ -439,6 +461,7 @@ pub(crate) struct AppServerRunRequest<'a> {
pub(crate) max_turns: u32,
pub(crate) timeout: Duration,
pub(crate) process_env: AppServerProcessEnv,
pub(crate) allow_unverified_codex: bool,
pub(crate) continuation_user_input: Option<String>,
pub(crate) activity_marker_path: Option<PathBuf>,
pub(crate) resume_thread_id: Option<String>,
Expand Down Expand Up @@ -988,6 +1011,7 @@ enum AppServerDynamicToolFailureKind {
#[serde(rename_all = "snake_case")]
enum AppServerCapabilityPreflightStatus {
Ok,
Warning,
Blocked,
}

Expand Down Expand Up @@ -1097,7 +1121,10 @@ pub(crate) fn archive_app_server_thread_after_success(
result
}

pub(crate) fn probe_app_server(listen: &str) -> crate::prelude::Result<AppServerRunResult> {
pub(crate) fn probe_app_server(
listen: &str,
allow_unverified_codex: bool,
) -> crate::prelude::Result<AppServerRunResult> {
let state_store = StateStore::open_in_memory()?;
let probe_tool_handler = ProbeDynamicToolHandler;
let result = execute_app_server_run(
Expand All @@ -1113,6 +1140,7 @@ pub(crate) fn probe_app_server(listen: &str) -> crate::prelude::Result<AppServer
max_turns: 1,
timeout: PROBE_TIMEOUT,
process_env: AppServerProcessEnv::default(),
allow_unverified_codex,
continuation_user_input: None,
activity_marker_path: None,
resume_thread_id: None,
Expand Down Expand Up @@ -2268,6 +2296,7 @@ fn execute_app_server_run_inner(
&mut recorder,
&request.cwd,
&initialize_response.user_agent,
request.allow_unverified_codex,
)?;

write_activity_marker_best_effort_for_request(request);
Expand Down Expand Up @@ -2359,6 +2388,7 @@ fn run_app_server_capability_preflight(
recorder: &mut RunRecorder<'_>,
cwd: &str,
user_agent: &str,
allow_unverified_codex: bool,
) -> crate::prelude::Result<AppServerCapabilityPreflightReport> {
let mut report = AppServerCapabilityPreflightReport::new();
let config = preflight_request(recorder, &report, "config/read", || {
Expand Down Expand Up @@ -2417,7 +2447,7 @@ fn run_app_server_capability_preflight(
}

if !report.has_blockers() {
record_app_server_compatibility_guard(&mut report, user_agent);
record_app_server_compatibility_guard(&mut report, user_agent, allow_unverified_codex);
}

record_app_server_preflight_report(recorder, &report)?;
Expand Down Expand Up @@ -2840,12 +2870,14 @@ fn record_mcp_preflight_degraded(report: &mut AppServerCapabilityPreflightReport
fn record_app_server_compatibility_guard(
report: &mut AppServerCapabilityPreflightReport,
user_agent: &str,
allow_unverified_codex: bool,
) {
let codex_cli_version = codex_cli_version_from_user_agent(user_agent);
let mut details = BTreeMap::new();

details.insert(String::from("user_agent"), user_agent.to_owned());
details.insert(String::from("supported_versions"), supported_codex_cli_versions_display());
details.insert(String::from("allow_unverified_codex"), allow_unverified_codex.to_string());
details
.insert(String::from("support_claim"), APP_SERVER_COMPATIBILITY_SUPPORT_CLAIM.to_owned());
details.insert(String::from("evidence"), APP_SERVER_COMPATIBILITY_EVIDENCE.to_owned());
Expand All @@ -2860,6 +2892,13 @@ fn record_app_server_compatibility_guard(
"app-server userAgent is within the locally verified Codex CLI capability range.",
details,
);
} else if allow_unverified_codex {
details.insert(String::from("override"), String::from("allow_unverified_codex"));
report.push_warning(
PREFLIGHT_CHECK_COMPATIBILITY,
"app-server userAgent is outside the locally verified Codex CLI capability range; continuing because unverified Codex versions are explicitly allowed.",
details,
);
} else {
report.push_blocked(
PREFLIGHT_CHECK_COMPATIBILITY,
Expand All @@ -2886,6 +2925,11 @@ fn codex_cli_version_from_user_agent(user_agent: &str) -> Option<String> {

return user_agent_version_token(&user_agent[marker_end..]);
}
if let Some(marker_start) = lower_user_agent.find("codex desktop/") {
let marker_end = marker_start + "codex desktop/".len();

return user_agent_version_token(&user_agent[marker_end..]);
}

let first_token = user_agent.split_whitespace().next()?;
let (product, version) = first_token.rsplit_once('/')?;
Expand Down
40 changes: 36 additions & 4 deletions apps/decodex/src/agent/app_server/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,15 +289,27 @@ fn app_server_compatibility_guard_accepts_current_verified_codex_surfaces() {
for (user_agent, expected_codex_cli_version) in [
("codex-cli 0.136.0", "codex-cli 0.136.0"),
("codex-cli 0.136.0-alpha.2", "codex-cli 0.136.0-alpha.2"),
(
"Codex Desktop/0.136.0 (Mac OS 26.5.1; arm64) ghostty/1.3.2-main-_6246c288a (decodex; 0.1.0)",
"codex-cli 0.136.0",
),
(
"Codex Desktop/0.137.0-alpha.4 (Mac OS 26.5.1; arm64) ghostty/1.3.2-main-_6246c288a (decodex; 0.1.0)",
"codex-cli 0.137.0-alpha.4",
),
("decodex/0.136.0 (Mac OS 26.5.0; arm64) unknown (decodex; 0.1.0)", "codex-cli 0.136.0"),
(
"decodex/0.136.0-alpha.2 (Mac OS 26.5.0; arm64) unknown (decodex; 0.1.0)",
"codex-cli 0.136.0-alpha.2",
),
(
"decodex/0.137.0-alpha.4 (Mac OS 26.5.1; arm64) unknown (decodex; 0.1.0)",
"codex-cli 0.137.0-alpha.4",
),
] {
let mut report = AppServerCapabilityPreflightReport::new();

super::record_app_server_compatibility_guard(&mut report, user_agent);
super::record_app_server_compatibility_guard(&mut report, user_agent, false);

assert!(!report.has_blockers(), "{user_agent} should be supported");
assert_eq!(report.compatibility_status(), "supported");
Expand All @@ -308,7 +320,7 @@ fn app_server_compatibility_guard_accepts_current_verified_codex_surfaces() {
);
assert_eq!(
report.compatibility_supported_versions(),
Some("codex-cli 0.136.0, codex-cli 0.136.0-alpha.2")
Some("codex-cli 0.136.0, codex-cli 0.136.0-alpha.2, codex-cli 0.137.0-alpha.4")
);
}
}
Expand All @@ -317,13 +329,14 @@ fn app_server_compatibility_guard_accepts_current_verified_codex_surfaces() {
fn app_server_compatibility_guard_rejects_unverified_codex_surfaces() {
for user_agent in [
"codex-cli 0.137.0-alpha.0",
"codex-cli 0.137.0-alpha.5",
"codex-cli 0.136.1",
"other-app/0.136.0",
"openai/codex upstream-main-post-rust-v0.136.0",
] {
let mut report = AppServerCapabilityPreflightReport::new();

super::record_app_server_compatibility_guard(&mut report, user_agent);
super::record_app_server_compatibility_guard(&mut report, user_agent, false);

assert!(report.has_blockers(), "{user_agent} should be outside support");
assert_eq!(report.compatibility_status(), "unsupported");
Expand All @@ -333,6 +346,22 @@ fn app_server_compatibility_guard_rejects_unverified_codex_surfaces() {
}
}

#[test]
fn app_server_compatibility_guard_allows_unverified_codex_when_requested() {
let mut report = AppServerCapabilityPreflightReport::new();

super::record_app_server_compatibility_guard(&mut report, "codex-cli 0.138.0-alpha.1", true);

assert!(!report.has_blockers());
assert_eq!(report.compatibility_status(), "unverified_allowed");
assert_eq!(report.checks()[0].name, "compatibility");
assert_eq!(report.checks()[0].status, super::AppServerCapabilityPreflightStatus::Warning);
assert_eq!(
report.checks()[0].details.get("override").map(String::as_str),
Some("allow_unverified_codex")
);
}

#[test]
fn app_server_compatibility_versions_match_spec_table() {
let spec_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
Expand Down Expand Up @@ -453,6 +482,7 @@ fn minimal_run_request<'a>() -> super::AppServerRunRequest<'a> {
max_turns: 1,
timeout: Duration::from_secs(30),
process_env: AppServerProcessEnv::default(),
allow_unverified_codex: false,
continuation_user_input: None,
activity_marker_path: None,
resume_thread_id: None,
Expand Down Expand Up @@ -2057,6 +2087,7 @@ fn live_app_server_resume_round_trip_updates_marker_and_state() {
max_turns: 3,
timeout: Duration::from_secs(30),
process_env: AppServerProcessEnv::default(),
allow_unverified_codex: false,
continuation_user_input: Some(String::from(
"Call `echo_resume` with `{\\\"text\\\":\\\"SECOND_OK\\\"}`. After the tool succeeds, reply with the exact text DONE.",
)),
Expand Down Expand Up @@ -2104,7 +2135,8 @@ fn live_app_server_resume_round_trip_updates_marker_and_state() {
max_turns: 1,
timeout: Duration::from_secs(30),
process_env: AppServerProcessEnv::default(),
continuation_user_input: None,
allow_unverified_codex: false,
continuation_user_input: None,
activity_marker_path: Some(marker_path.clone()),
resume_thread_id: Some(first_result.thread_id.clone()),
ephemeral_thread: false,
Expand Down
5 changes: 3 additions & 2 deletions apps/decodex/src/agent/tracker_tool_bridge/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -565,11 +565,12 @@ impl<'a> TrackerToolBridge<'a> {
&projection,
) {
Ok(comment_created) => comment_created,
Err(error) =>
Err(error) => {
return Err(format!(
"Failed to record an execution-state checkpoint for issue `{}`: {error}",
self.issue.identifier
)),
));
},
};

state_store.record_linear_execution_event(&projection.record).map_err(|error| {
Expand Down
Loading