From e173c7eb3f5075ecf0a3dbb0e6a8c45790b1e068 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Wed, 3 Jun 2026 10:20:00 +0800 Subject: [PATCH 1/3] {"schema":"decodex/commit/1","summary":"Accept Codex Desktop app-server userAgent","authority":"manual"} --- apps/decodex/src/agent/app_server.rs | 20 ++++++++++++++++---- apps/decodex/src/agent/app_server/tests.rs | 15 ++++++++++++++- docs/spec/app-server.md | 3 ++- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/apps/decodex/src/agent/app_server.rs b/apps/decodex/src/agent/app_server.rs index fc28c7c..e7d9b37 100644 --- a/apps/decodex/src/agent/app_server.rs +++ b/apps/decodex/src/agent/app_server.rs @@ -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"; @@ -2886,6 +2893,11 @@ fn codex_cli_version_from_user_agent(user_agent: &str) -> Option { 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('/')?; diff --git a/apps/decodex/src/agent/app_server/tests.rs b/apps/decodex/src/agent/app_server/tests.rs index 054ecad..8713c6b 100644 --- a/apps/decodex/src/agent/app_server/tests.rs +++ b/apps/decodex/src/agent/app_server/tests.rs @@ -289,11 +289,23 @@ 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(); @@ -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") ); } } @@ -317,6 +329,7 @@ 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", diff --git a/docs/spec/app-server.md b/docs/spec/app-server.md index 4b240fd..a5dc7b9 100644 --- a/docs/spec/app-server.md +++ b/docs/spec/app-server.md @@ -53,12 +53,13 @@ range only when all of these are true: - The executable Decodex compatibility guard reports `compatibility=supported` for the initialized app-server `userAgent` after the capability preflight succeeds. -As of the 2026-06-02 self-compatibility pass, the verified local range is: +As of the 2026-06-03 self-compatibility pass, the verified local range is: | Codex surface | Version | Evidence | | --- | --- | --- | | `PATH` `codex` | `codex-cli 0.136.0` | Generated `--experimental` schema contains the required methods and fields; `decodex probe stdio://` returned `PROBE_OK`. | | Codex Beta app bundled `codex` | `codex-cli 0.136.0-alpha.2` | Running `decodex probe stdio://` with the bundle resource directory first on `PATH` returned `PROBE_OK`. | +| Codex Desktop app-server | `codex-cli 0.137.0-alpha.4` | Generated `--experimental` schema contains the required schema bundle; `decodex probe stdio://` returned `PROBE_OK` with initialized `userAgent = "Codex Desktop/0.137.0-alpha.4 ..."`. | The same pass compared that range against upstream Codex: From 2e9aa23da58fac0257350e37836508a2bbc368bd Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Wed, 3 Jun 2026 11:55:48 +0800 Subject: [PATCH 2/3] {"schema":"decodex/commit/1","summary":"Allow unverified Codex app-server dogfood","authority":"manual"} --- Makefile.toml | 64 ++++++------ README.md | 3 + apps/decodex/src/agent/app_server.rs | 36 ++++++- apps/decodex/src/agent/app_server/tests.rs | 25 ++++- .../src/agent/tracker_tool_bridge/tools.rs | 5 +- apps/decodex/src/cli.rs | 46 ++++++++- apps/decodex/src/orchestrator/daemon.rs | 89 +++++++++-------- apps/decodex/src/orchestrator/entrypoints.rs | 52 +++++++--- apps/decodex/src/orchestrator/execution.rs | 22 ++++- apps/decodex/src/orchestrator/run_cycle.rs | 98 +++++++++++++------ .../tests/intake/candidate_selection.rs | 5 + .../tests/intake/prepare_issue_run.rs | 15 +-- .../tests/intake/run_and_prompting.rs | 12 ++- .../tests/intake/workflow_reload.rs | 1 + .../tests/recovery/closeout/dispatch.rs | 6 +- .../tests/recovery/closeout/identity.rs | 5 + .../tests/recovery/runtime_reentry.rs | 46 ++++----- .../orchestrator/tests/retry/scheduling.rs | 5 + .../src/orchestrator/tests/retry/selection.rs | 2 + .../src/orchestrator/tests/runtime/failure.rs | 7 +- apps/decodex/src/orchestrator/types.rs | 5 + apps/decodex/src/radar.rs | 5 +- apps/decodex/src/tracker/linear.rs | 5 +- docs/spec/app-server.md | 5 + docs/spec/runtime.md | 4 +- plugins/decodex/skills/manual-cli/SKILL.md | 3 + scripts/README.md | 2 + scripts/macos/test_decodex_app_stage.sh | 34 +++++++ 28 files changed, 430 insertions(+), 177 deletions(-) create mode 100755 scripts/macos/test_decodex_app_stage.sh diff --git a/Makefile.toml b/Makefile.toml index 42242e9..bc8671e 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -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 | | @@ -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] @@ -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 @@ -208,7 +231,7 @@ args = [ # | ----------------------- | --------- | --- | # | test | composite | | # | test-rust | command | | -# | test-decodex-app-stage | script | | +# | test-decodex-app-stage | command | | [tasks.test] clear = true @@ -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 | diff --git a/README.md b/README.md index 7e99265..e290d37 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,9 @@ cargo run -p decodex --bin decodex -- serve --listen-address 127.0.0.1:8912 Project-scoped commands accept `--config ` 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 diff --git a/apps/decodex/src/agent/app_server.rs b/apps/decodex/src/agent/app_server.rs index e7d9b37..571fec6 100644 --- a/apps/decodex/src/agent/app_server.rs +++ b/apps/decodex/src/agent/app_server.rs @@ -185,6 +185,20 @@ impl AppServerCapabilityPreflightReport { }); } + fn push_warning( + &mut self, + name: &'static str, + summary: impl Into, + details: BTreeMap, + ) { + 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) } @@ -203,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", } @@ -446,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, pub(crate) activity_marker_path: Option, pub(crate) resume_thread_id: Option, @@ -995,6 +1011,7 @@ enum AppServerDynamicToolFailureKind { #[serde(rename_all = "snake_case")] enum AppServerCapabilityPreflightStatus { Ok, + Warning, Blocked, } @@ -1104,7 +1121,10 @@ pub(crate) fn archive_app_server_thread_after_success( result } -pub(crate) fn probe_app_server(listen: &str) -> crate::prelude::Result { +pub(crate) fn probe_app_server( + listen: &str, + allow_unverified_codex: bool, +) -> crate::prelude::Result { let state_store = StateStore::open_in_memory()?; let probe_tool_handler = ProbeDynamicToolHandler; let result = execute_app_server_run( @@ -1120,6 +1140,7 @@ pub(crate) fn probe_app_server(listen: &str) -> crate::prelude::Result, cwd: &str, user_agent: &str, + allow_unverified_codex: bool, ) -> crate::prelude::Result { let mut report = AppServerCapabilityPreflightReport::new(); let config = preflight_request(recorder, &report, "config/read", || { @@ -2424,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)?; @@ -2847,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()); @@ -2867,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, diff --git a/apps/decodex/src/agent/app_server/tests.rs b/apps/decodex/src/agent/app_server/tests.rs index 8713c6b..380f72d 100644 --- a/apps/decodex/src/agent/app_server/tests.rs +++ b/apps/decodex/src/agent/app_server/tests.rs @@ -309,7 +309,7 @@ fn app_server_compatibility_guard_accepts_current_verified_codex_surfaces() { ] { 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"); @@ -336,7 +336,7 @@ fn app_server_compatibility_guard_rejects_unverified_codex_surfaces() { ] { 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"); @@ -346,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")) @@ -466,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, @@ -2070,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.", )), @@ -2117,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, diff --git a/apps/decodex/src/agent/tracker_tool_bridge/tools.rs b/apps/decodex/src/agent/tracker_tool_bridge/tools.rs index 2980798..8b05e57 100644 --- a/apps/decodex/src/agent/tracker_tool_bridge/tools.rs +++ b/apps/decodex/src/agent/tracker_tool_bridge/tools.rs @@ -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| { diff --git a/apps/decodex/src/cli.rs b/apps/decodex/src/cli.rs index 8bf66b0..3e875fe 100644 --- a/apps/decodex/src/cli.rs +++ b/apps/decodex/src/cli.rs @@ -89,6 +89,8 @@ pub(crate) struct AttemptRequest { pub(crate) initial_issue_state: Option, #[serde(default)] pub(crate) lease_preacquired: bool, + #[serde(default)] + pub(crate) allow_unverified_codex: bool, pub(crate) issue_claim_fd: Option, pub(crate) dispatch_slot_fd: Option, pub(crate) dispatch_slot_index: Option, @@ -298,6 +300,9 @@ struct RunCommand { /// Explain current queued candidates without preparing or dispatching a lane. #[arg(long, requires = "dry_run", conflicts_with = "issue")] explain: bool, + /// Continue after warning when Codex app-server is outside the locally verified range. + #[arg(long)] + allow_unverified_codex: bool, } impl RunCommand { fn run(&self) -> Result<()> { @@ -317,6 +322,7 @@ impl RunCommand { preferred_attempt_number: None, preferred_retry_budget_base: None, preferred_workflow_snapshot: None, + allow_unverified_codex: self.allow_unverified_codex, }) } } @@ -331,6 +337,9 @@ struct ServeCommand { /// Start the local dev endpoint without polling or dispatching projects. #[arg(long, hide = true)] dev: bool, + /// Continue after warning when Codex app-server is outside the locally verified range. + #[arg(long)] + allow_unverified_codex: bool, } impl ServeCommand { fn run(&self) -> Result<()> { @@ -338,6 +347,7 @@ impl ServeCommand { config_path: self.project_config.as_path(), listen_address: &self.listen_address, dev: self.dev, + allow_unverified_codex: self.allow_unverified_codex, }) } } @@ -1207,10 +1217,13 @@ struct ProbeCommand { /// Override the expected app-server transport during probing. #[arg(value_name = "TRANSPORT", default_value = "stdio://")] transport: String, + /// Continue after warning when Codex app-server is outside the locally verified range. + #[arg(long)] + allow_unverified_codex: bool, } impl ProbeCommand { fn run(&self) -> Result<()> { - let report = agent::probe_app_server(&self.transport)?; + let report = agent::probe_app_server(&self.transport, self.allow_unverified_codex)?; println!( "probe ok: compatibility={} codex_version={} supported_versions=\"{}\" thread={} turn={} events={} output={}", @@ -1263,6 +1276,7 @@ impl AttemptCommand { preferred_attempt_number: Some(request.attempt_number), preferred_retry_budget_base: Some(request.retry_budget_base), preferred_workflow_snapshot: Some(request.workflow_snapshot.as_str()), + allow_unverified_codex: request.allow_unverified_codex, }) } } @@ -1652,18 +1666,44 @@ mod tests { ]); assert!(matches!( - cli.command, + cli.command, Command::Serve(ServeCommand { project_config: ProjectConfigArgs { config: Some(config) }, listen_address, dev, + allow_unverified_codex, }) if listen_address == "127.0.0.1:9000" && !dev + && !allow_unverified_codex && config == Path::new("./project.toml") )); } + #[test] + fn parses_runtime_unverified_codex_override() { + let cli = Cli::parse_from(["decodex", "run", "--allow-unverified-codex"]); + + assert!(matches!( + cli.command, + Command::Run(RunCommand { allow_unverified_codex: true, .. }) + )); + + let cli = Cli::parse_from(["decodex", "serve", "--allow-unverified-codex"]); + + assert!(matches!( + cli.command, + Command::Serve(ServeCommand { allow_unverified_codex: true, .. }) + )); + + let cli = Cli::parse_from(["decodex", "probe", "--allow-unverified-codex"]); + + assert!(matches!( + cli.command, + Command::Probe(ProbeCommand { allow_unverified_codex: true, .. }) + )); + } + #[test] fn parses_serve_dev() { let cli = Cli::parse_from(["decodex", "serve", "--dev"]); @@ -2031,7 +2071,7 @@ mod tests { assert!(matches!( cli.command, - Command::Probe(ProbeCommand { transport }) if transport == "ws://127.0.0.1:9000" + Command::Probe(ProbeCommand { transport, .. }) if transport == "ws://127.0.0.1:9000" )); } diff --git a/apps/decodex/src/orchestrator/daemon.rs b/apps/decodex/src/orchestrator/daemon.rs index df3ef77..d4ae350 100644 --- a/apps/decodex/src/orchestrator/daemon.rs +++ b/apps/decodex/src/orchestrator/daemon.rs @@ -14,6 +14,7 @@ struct DaemonTickRuntimeContext<'a, T, I> { worktree_manager: &'a WorktreeManager, review_state_inspector: &'a I, recoverable_worktree_skip_cache: Option<&'a mut RecoverableWorktreeSkipCache>, + allow_unverified_codex: bool, } fn load_daemon_tick_context( @@ -76,6 +77,7 @@ fn run_daemon_tick( retry_queue: &mut RetryQueue, recoverable_worktree_skip_cache: &mut RecoverableWorktreeSkipCache, context: &DaemonTickContext, + allow_unverified_codex: bool, ) -> Result<()> { let review_state_inspector = GhPullRequestReviewStateInspector { github_token_env_var: Some(context.config.github().token_env_var().to_owned()), @@ -90,10 +92,11 @@ fn run_daemon_tick( tracker: &context.tracker, project: &context.config, workflow: &context.workflow, - worktree_manager: &context.worktree_manager, - review_state_inspector: &review_state_inspector, - recoverable_worktree_skip_cache: Some(recoverable_worktree_skip_cache), - }, + worktree_manager: &context.worktree_manager, + review_state_inspector: &review_state_inspector, + recoverable_worktree_skip_cache: Some(recoverable_worktree_skip_cache), + allow_unverified_codex, + }, ) } @@ -146,15 +149,8 @@ where .max_concurrent_agents() .has_capacity(active_children.len()) { - if !spawn_next_daemon_child( - config_path, - state_store, - active_children, - retry_queue, - context.tracker, - context.project, - context.workflow, - )? { + if !spawn_next_daemon_child(config_path, state_store, active_children, retry_queue, &context)? + { break; } } @@ -516,29 +512,33 @@ fn spawn_next_daemon_child( state_store: &StateStore, active_children: &mut Vec, retry_queue: &mut RetryQueue, - tracker: &T, - project: &ServiceConfig, - workflow: &WorkflowDocument, + context: &DaemonTickRuntimeContext<'_, T, impl PullRequestReviewStateInspector>, ) -> Result where T: IssueTracker, { - let next_run = plan_next_daemon_run(retry_queue, tracker, project, workflow, state_store)?; + let next_run = plan_next_daemon_run( + retry_queue, + context.tracker, + context.project, + context.workflow, + state_store, + )?; match next_run { Some((summary, from_retry_queue)) => { if summary.dispatch_mode != IssueDispatchMode::Closeout { - ensure_project_has_no_merged_worktree_cleanup_debt(project)?; + ensure_project_has_no_merged_worktree_cleanup_debt(context.project)?; } state_store.configure_dispatch_slot_root( - project.service_id(), - project.worktree_root(), - workflow.frontmatter().execution().max_concurrent_agents(), + context.project.service_id(), + context.project.worktree_root(), + context.workflow.frontmatter().execution().max_concurrent_agents(), )?; if !state_store.try_acquire_lease( - project.service_id(), + context.project.service_id(), &summary.issue_id, &summary.run_id, &summary.issue_state, @@ -547,10 +547,10 @@ where } let daemon_spawn_state = - materialize_daemon_spawn_state(project, workflow, state_store, &summary) - .inspect_err(|_error| { - let _ = state_store.clear_lease(&summary.issue_id); - })?; + materialize_daemon_spawn_state(context.project, context.workflow, state_store, &summary) + .inspect_err(|_error| { + let _ = state_store.clear_lease(&summary.issue_id); + })?; state_store.record_run_attempt( &summary.run_id, @@ -559,7 +559,7 @@ where "starting", )?; state_store.upsert_worktree( - project.service_id(), + context.project.service_id(), &summary.issue_id, &daemon_spawn_state.worktree.branch_name, &daemon_spawn_state.worktree.path.display().to_string(), @@ -568,9 +568,10 @@ where let mut child = spawn_planned_daemon_child( config_path, state_store, - workflow, + context.workflow, &summary, daemon_spawn_state.retry_budget_base, + context.allow_unverified_codex, )?; if let Err(error) = state::write_run_operation_marker_for_process( @@ -607,7 +608,7 @@ where retry_project_slug: String::new(), dispatch_mode: summary.dispatch_mode, from_retry_queue, - workflow: workflow.clone(), + workflow: context.workflow.clone(), }); Ok(true) @@ -630,6 +631,7 @@ fn spawn_planned_daemon_child( workflow: &WorkflowDocument, summary: &RunSummary, retry_budget_base: i64, + allow_unverified_codex: bool, ) -> Result { let issue_claim_handoff = Some(state_store.clone_issue_claim_for_child(&summary.issue_id).inspect_err(|_error| { @@ -647,10 +649,11 @@ fn spawn_planned_daemon_child( preferred_initial_issue_state: Some(summary.initial_issue_state.as_str()), dispatch_mode: summary.dispatch_mode, preferred_run_id: summary.run_id.as_str(), - preferred_attempt_number: summary.attempt_number, - preferred_retry_budget_base: retry_budget_base, - workflow, - issue_claim_handoff: issue_claim_handoff.as_ref(), + preferred_attempt_number: summary.attempt_number, + preferred_retry_budget_base: retry_budget_base, + workflow, + allow_unverified_codex, + issue_claim_handoff: issue_claim_handoff.as_ref(), dispatch_slot_handoff: dispatch_slot_handoff.as_ref(), dispatch_slot_index_handoff, }) @@ -778,11 +781,12 @@ fn spawn_run_once_child(request: SpawnRunOnceChildRequest<'_>) -> Result dispatch_slot_fd: None, dispatch_slot_index: request.dispatch_slot_index_handoff, dispatch_mode: request.dispatch_mode.into(), - run_id: String::from(request.preferred_run_id), - attempt_number: request.preferred_attempt_number, - retry_budget_base: request.preferred_retry_budget_base, - workflow_snapshot: request.workflow.to_markdown()?, - }; + run_id: String::from(request.preferred_run_id), + attempt_number: request.preferred_attempt_number, + retry_budget_base: request.preferred_retry_budget_base, + allow_unverified_codex: request.allow_unverified_codex, + workflow_snapshot: request.workflow.to_markdown()?, + }; let payload = serde_json::to_vec(&attempt_request)?; let mut command = Command::new(executable); @@ -866,10 +870,11 @@ where preferred_issue_claim_fd: None, preferred_dispatch_slot_fd: None, preferred_dispatch_slot_index: None, - dispatch_mode: entry.dispatch_mode, - preferred_run_identity: None, - preferred_retry_budget_base: None, - })? + dispatch_mode: entry.dispatch_mode, + preferred_run_identity: None, + preferred_retry_budget_base: None, + allow_unverified_codex: false, + })? else { if retry_entry_is_temporarily_blocked(tracker, project, workflow, state_store, &entry)? { diff --git a/apps/decodex/src/orchestrator/entrypoints.rs b/apps/decodex/src/orchestrator/entrypoints.rs index 32d4a9c..fc20184 100644 --- a/apps/decodex/src/orchestrator/entrypoints.rs +++ b/apps/decodex/src/orchestrator/entrypoints.rs @@ -123,10 +123,11 @@ pub(crate) fn run_once(request: RunOnceRequest<'_>) -> Result<()> { preferred_dispatch_slot_fd: request.preferred_dispatch_slot_fd, preferred_dispatch_slot_index: request.preferred_dispatch_slot_index, preferred_dispatch_mode: request.preferred_dispatch_mode, - preferred_run_identity, - preferred_retry_budget_base: request.preferred_retry_budget_base, - preferred_workflow_snapshot: request.preferred_workflow_snapshot, - }) { + preferred_run_identity, + preferred_retry_budget_base: request.preferred_retry_budget_base, + preferred_workflow_snapshot: request.preferred_workflow_snapshot, + allow_unverified_codex: request.allow_unverified_codex, + }) { Ok(summary) => summary, Err(error) => { let Some(backoff) = tracker_rate_limit_backoff(&error, Instant::now(), "run_cycle") @@ -243,8 +244,12 @@ pub(crate) fn run_control_plane(request: ServeRequest<'_>) -> Result<()> { let linear_scan_requests = drain_operator_linear_scan_requests_best_effort(&operator_state_endpoint); - let snapshot = - run_control_plane_tick(&state_store, &mut project_runtimes, &linear_scan_requests)?; + let snapshot = run_control_plane_tick_with_options( + &state_store, + &mut project_runtimes, + &linear_scan_requests, + request.allow_unverified_codex, + )?; publish_operator_snapshot(&operator_state_endpoint, &snapshot); sleep_until_next_tick(DEFAULT_CONTROL_PLANE_POLL_INTERVAL, tick_started_at); @@ -749,10 +754,20 @@ where Ok(snapshot) } +#[cfg(test)] fn run_control_plane_tick( state_store: &StateStore, project_runtimes: &mut HashMap, linear_scan_requests: &[OperatorLinearScanRequest], +) -> Result { + run_control_plane_tick_with_options(state_store, project_runtimes, linear_scan_requests, false) +} + +fn run_control_plane_tick_with_options( + state_store: &StateStore, + project_runtimes: &mut HashMap, + linear_scan_requests: &[OperatorLinearScanRequest], + allow_unverified_codex: bool, ) -> Result { let registered_projects = state_store.list_projects()?; let now = Instant::now(); @@ -763,12 +778,13 @@ fn run_control_plane_tick( run_control_plane_project_tick( project, state_store, - runtime, - project_warnings, - linear_scan_requests, - now, - ) - })) + runtime, + project_warnings, + linear_scan_requests, + now, + allow_unverified_codex, + ) + })) } fn drain_operator_linear_scan_requests_best_effort( @@ -940,6 +956,7 @@ fn run_control_plane_project_tick( snapshot_warnings: &mut Vec<&'static str>, linear_scan_requests: &[OperatorLinearScanRequest], now: Instant, + allow_unverified_codex: bool, ) -> ControlPlaneProjectTick { if tracker_backoff_active(runtime, now) { snapshot_warnings.push(TRACKER_RATE_LIMIT_WARNING); @@ -977,7 +994,14 @@ fn run_control_plane_project_tick( match load_daemon_tick_context(project.config_path(), &mut runtime.workflow_cache) { Ok(context) => - control_plane_project_snapshot(project, state_store, runtime, &context, snapshot_warnings), + control_plane_project_snapshot( + project, + state_store, + runtime, + &context, + snapshot_warnings, + allow_unverified_codex, + ), Err(error) => { let _ = error; @@ -1255,6 +1279,7 @@ fn control_plane_project_snapshot( runtime: &mut ProjectDaemonRuntime, context: &DaemonTickContext, snapshot_warnings: &mut Vec<&'static str>, + allow_unverified_codex: bool, ) -> ControlPlaneProjectTick { if let Err(error) = run_daemon_tick( project.config_path(), @@ -1263,6 +1288,7 @@ fn control_plane_project_snapshot( &mut runtime.retry_queue, &mut runtime.recoverable_worktree_skip_cache, context, + allow_unverified_codex, ) { if let Some(connector_backoff) = remember_tracker_backoff( runtime, diff --git a/apps/decodex/src/orchestrator/execution.rs b/apps/decodex/src/orchestrator/execution.rs index b1ae163..74a5b23 100644 --- a/apps/decodex/src/orchestrator/execution.rs +++ b/apps/decodex/src/orchestrator/execution.rs @@ -143,6 +143,7 @@ fn execute_issue_run( workflow: &WorkflowDocument, state_store: &StateStore, issue_run: IssueRunPlan, + allow_unverified_codex: bool, ) -> Result where T: IssueTracker, @@ -166,7 +167,16 @@ where )?; let result = ensure_automation_activity_label(tracker, &issue_run.issue, project.service_id(), true) - .and_then(|_| execute_issue_run_inner(tracker, project, workflow, state_store, &issue_run)); + .and_then(|_| { + execute_issue_run_inner( + tracker, + project, + workflow, + state_store, + &issue_run, + allow_unverified_codex, + ) + }); state_store.clear_lease(&issue_run.issue.id)?; @@ -500,6 +510,7 @@ fn execute_issue_run_inner( workflow: &WorkflowDocument, state_store: &StateStore, issue_run: &IssueRunPlan, + allow_unverified_codex: bool, ) -> Result where T: IssueTracker, @@ -571,10 +582,11 @@ where issue_run, &review_context, ), - max_turns: workflow.frontmatter().execution().max_turns(), - timeout: ACTIVE_RUN_IDLE_TIMEOUT, - process_env: agent_git_credentials.process_env().clone(), - continuation_user_input: Some(build_continuation_user_input( + max_turns: workflow.frontmatter().execution().max_turns(), + timeout: ACTIVE_RUN_IDLE_TIMEOUT, + process_env: agent_git_credentials.process_env().clone(), + allow_unverified_codex, + continuation_user_input: Some(build_continuation_user_input( &issue_run.issue, workflow, issue_run.dispatch_mode, diff --git a/apps/decodex/src/orchestrator/run_cycle.rs b/apps/decodex/src/orchestrator/run_cycle.rs index 8274f8e..2ac9e8b 100644 --- a/apps/decodex/src/orchestrator/run_cycle.rs +++ b/apps/decodex/src/orchestrator/run_cycle.rs @@ -95,10 +95,11 @@ fn run_configured_cycle( preferred_issue_claim_fd: request.preferred_issue_claim_fd, preferred_dispatch_slot_fd: request.preferred_dispatch_slot_fd, preferred_dispatch_slot_index: request.preferred_dispatch_slot_index, - dispatch_mode: request.preferred_dispatch_mode.unwrap_or(IssueDispatchMode::Normal), - preferred_run_identity: request.preferred_run_identity, - preferred_retry_budget_base: request.preferred_retry_budget_base, - }; + dispatch_mode: request.preferred_dispatch_mode.unwrap_or(IssueDispatchMode::Normal), + preferred_run_identity: request.preferred_run_identity, + preferred_retry_budget_base: request.preferred_retry_budget_base, + allow_unverified_codex: request.allow_unverified_codex, + }; return match request.preferred_dispatch_mode { Some(_) => run_target_issue_once(target_context), @@ -106,7 +107,14 @@ fn run_configured_cycle( }; } - run_project_once(&tracker, &config, &workflow, request.state_store, request.dry_run) + run_project_once( + &tracker, + &config, + &workflow, + request.state_store, + request.dry_run, + request.allow_unverified_codex, + ) } fn load_configured_cycle_workflow( @@ -127,11 +135,20 @@ fn run_project_once( workflow: &WorkflowDocument, state_store: &StateStore, dry_run: bool, + allow_unverified_codex: bool, ) -> Result> where T: IssueTracker, { - run_project_once_with_exclusions(tracker, project, workflow, state_store, dry_run, &[]) + run_project_once_with_exclusions( + tracker, + project, + workflow, + state_store, + dry_run, + &[], + allow_unverified_codex, + ) } fn run_project_once_with_exclusions( @@ -141,6 +158,7 @@ fn run_project_once_with_exclusions( state_store: &StateStore, dry_run: bool, excluded_issue_ids: &[&str], + allow_unverified_codex: bool, ) -> Result> where T: IssueTracker, @@ -157,7 +175,15 @@ where return Ok(None); }; - complete_issue_run(tracker, project, workflow, state_store, issue_run, dry_run) + complete_issue_run( + tracker, + project, + workflow, + state_store, + issue_run, + dry_run, + allow_unverified_codex, + ) } fn reconcile_post_review_orchestration( @@ -1215,15 +1241,15 @@ where return Ok(None); } - return replan_project_issue_run_after_excluding( - tracker, - project, - workflow, - state_store, - dry_run, - excluded_issue_ids, - issue.id.as_str(), - ); + return replan_project_issue_run_after_excluding( + tracker, + project, + workflow, + state_store, + dry_run, + excluded_issue_ids, + issue.id.as_str(), + ); } let Some(issue_run) = prepare_issue_run( @@ -1243,9 +1269,9 @@ where run_id: identity.run_id.as_str(), attempt_number: identity.attempt_number, } - }), - preferred_retry_budget_base: None, - }, + }), + preferred_retry_budget_base: None, + }, issue, )? else { @@ -1779,9 +1805,9 @@ where dispatch_mode: context.dispatch_mode, preferred_issue_state: context.preferred_issue_state, preferred_initial_issue_state: context.preferred_initial_issue_state, - preferred_run_identity, - preferred_retry_budget_base: context.preferred_retry_budget_base, - }, + preferred_run_identity, + preferred_retry_budget_base: context.preferred_retry_budget_base, + }, issue, )? else { @@ -1793,9 +1819,10 @@ where context.project, context.workflow, context.state_store, - issue_run, - context.dry_run, - ) + issue_run, + context.dry_run, + context.allow_unverified_codex, + ) } fn ensure_target_closeout_dispatch_is_unblocked( @@ -1925,6 +1952,7 @@ where dispatch_mode: IssueDispatchMode::Closeout, preferred_run_identity, preferred_retry_budget_base: context.preferred_retry_budget_base, + allow_unverified_codex: context.allow_unverified_codex, }) } @@ -2015,6 +2043,7 @@ fn target_issue_run_context_with_dispatch_mode<'a, T>( dispatch_mode, preferred_run_identity: context.preferred_run_identity, preferred_retry_budget_base: context.preferred_retry_budget_base, + allow_unverified_codex: context.allow_unverified_codex, } } @@ -2300,6 +2329,7 @@ fn complete_issue_run( state_store: &StateStore, issue_run: IssueRunPlan, dry_run: bool, + allow_unverified_codex: bool, ) -> Result> where T: IssueTracker, @@ -2308,7 +2338,14 @@ where return Ok(Some(run_summary_from_issue_run(project.service_id(), &issue_run))); } - let summary = execute_issue_run(tracker, project, workflow, state_store, issue_run)?; + let summary = execute_issue_run( + tracker, + project, + workflow, + state_store, + issue_run, + allow_unverified_codex, + )?; let review_state_inspector = GhPullRequestReviewStateInspector { github_token_env_var: Some(project.github().token_env_var().to_owned()), }; @@ -2324,9 +2361,10 @@ where tracker, project, workflow, - state_store, - source_summary, - ), + state_store, + source_summary, + allow_unverified_codex, + ), )? { return Ok(Some(retained_summary)); } @@ -2340,6 +2378,7 @@ fn run_retained_closeout_for_handoff_summary( workflow: &WorkflowDocument, state_store: &StateStore, source_summary: &RunSummary, + allow_unverified_codex: bool, ) -> Result> where T: IssueTracker, @@ -2360,6 +2399,7 @@ where dispatch_mode: IssueDispatchMode::Closeout, preferred_run_identity: None, preferred_retry_budget_base: None, + allow_unverified_codex, }) } diff --git a/apps/decodex/src/orchestrator/tests/intake/candidate_selection.rs b/apps/decodex/src/orchestrator/tests/intake/candidate_selection.rs index 9d30208..31c16e7 100644 --- a/apps/decodex/src/orchestrator/tests/intake/candidate_selection.rs +++ b/apps/decodex/src/orchestrator/tests/intake/candidate_selection.rs @@ -289,6 +289,7 @@ fn plan_project_issue_run_prefers_post_review_repair_lane_over_normal_candidate( dispatch_mode: IssueDispatchMode::ReviewRepair, preferred_run_identity: None, preferred_retry_budget_base: None, + allow_unverified_codex: false, }) .expect("targeted review-repair planning should succeed") .expect("review-repair issue run should plan"); @@ -409,6 +410,7 @@ fn targeted_post_review_repair_skips_persisted_exhausted_retry_budget() { dispatch_mode: IssueDispatchMode::ReviewRepair, preferred_run_identity: None, preferred_retry_budget_base: None, + allow_unverified_codex: false, }) .expect("targeted review-repair planning should succeed"); @@ -563,6 +565,7 @@ fn plan_project_issue_run_prefers_post_review_closeout_lane_over_normal_candidat dispatch_mode: IssueDispatchMode::Closeout, preferred_run_identity: None, preferred_retry_budget_base: None, + allow_unverified_codex: false, }) .expect("targeted closeout planning should succeed") .expect("closeout issue run should plan"); @@ -685,6 +688,7 @@ fn plan_project_issue_run_allows_merged_closeout_after_retry_budget() { dispatch_mode: IssueDispatchMode::Closeout, preferred_run_identity: None, preferred_retry_budget_base: None, + allow_unverified_codex: false, }) .expect("targeted closeout planning should succeed") .expect("closeout issue run should plan"); @@ -1407,6 +1411,7 @@ fn non_dry_run_closeout_dispatch_errors_when_pr_state_read_fails() { dispatch_mode: IssueDispatchMode::Closeout, preferred_run_identity: None, preferred_retry_budget_base: None, + allow_unverified_codex: false, }) .expect_err("non-dry-run closeout dispatch should surface GH state read failures"); diff --git a/apps/decodex/src/orchestrator/tests/intake/prepare_issue_run.rs b/apps/decodex/src/orchestrator/tests/intake/prepare_issue_run.rs index 4165602..e291685 100644 --- a/apps/decodex/src/orchestrator/tests/intake/prepare_issue_run.rs +++ b/apps/decodex/src/orchestrator/tests/intake/prepare_issue_run.rs @@ -618,13 +618,14 @@ fn run_target_issue_once_skips_reconciliation_for_preacquired_child_runs() { preferred_issue_claim_fd: Some(child_issue_claim.into_raw_fd()), preferred_dispatch_slot_fd: Some(child_guard.into_raw_fd()), preferred_dispatch_slot_index: Some(child_slot_index), - dispatch_mode: IssueDispatchMode::Normal, - preferred_run_identity: Some(PreferredRunIdentity { - run_id: "planned-run", - attempt_number: 1, - }), - preferred_retry_budget_base: None, - }) + dispatch_mode: IssueDispatchMode::Normal, + preferred_run_identity: Some(PreferredRunIdentity { + run_id: "planned-run", + attempt_number: 1, + }), + preferred_retry_budget_base: None, + allow_unverified_codex: false, + }) .expect("targeted child run should not error before refresh lookup"); assert!(summary.is_none(), "missing refreshed issue should stop before execution"); diff --git a/apps/decodex/src/orchestrator/tests/intake/run_and_prompting.rs b/apps/decodex/src/orchestrator/tests/intake/run_and_prompting.rs index bdee17c..1e2dea2 100644 --- a/apps/decodex/src/orchestrator/tests/intake/run_and_prompting.rs +++ b/apps/decodex/src/orchestrator/tests/intake/run_and_prompting.rs @@ -90,7 +90,7 @@ fn dry_run_selects_one_issue_and_plans_worktree() { let (_temp_dir, config, workflow) = temp_project_layout(); let tracker = FakeTracker::new(vec![sample_issue("Todo", &[])]); let state_store = StateStore::open_in_memory().expect("state store should open"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("run once should succeed") .expect("one issue should be selected"); @@ -161,6 +161,7 @@ fn targeted_identifier_dispatch_accepts_status_ready_queued_issue() { dispatch_mode: IssueDispatchMode::Normal, preferred_run_identity: None, preferred_retry_budget_base: None, + allow_unverified_codex: false, }, ) .expect("targeted identifier run should succeed") @@ -194,6 +195,7 @@ fn targeted_inferred_dispatch_keeps_retry_for_active_issue() { dispatch_mode: IssueDispatchMode::Normal, preferred_run_identity: None, preferred_retry_budget_base: None, + allow_unverified_codex: false, }, ) .expect("targeted active identifier run should succeed") @@ -270,6 +272,7 @@ fn targeted_identifier_dispatch_accepts_status_visible_retained_closeout_lane() dispatch_mode: IssueDispatchMode::Normal, preferred_run_identity: None, preferred_retry_budget_base: None, + allow_unverified_codex: false, }, ) .expect("targeted retained closeout identifier run should succeed") @@ -361,6 +364,7 @@ fn targeted_identifier_dispatch_rejects_different_status_visible_closeout_lane() dispatch_mode: IssueDispatchMode::Normal, preferred_run_identity: None, preferred_retry_budget_base: None, + allow_unverified_codex: false, }, ) .expect_err("targeted closeout inference should reject a different visible lane"); @@ -401,7 +405,7 @@ fn dry_run_returns_none_when_intake_has_no_service_owned_candidate() { let tracker = FakeTracker::with_refresh_snapshots_and_project(vec![], vec![vec![]], false); let state_store = StateStore::open_in_memory().expect("state store should open"); let summary = - orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("dry run without queued issues should succeed"); assert!(summary.is_none(), "empty intake should simply produce no dry-run selection"); @@ -420,7 +424,7 @@ fn dry_run_returns_none_when_intake_has_no_service_owned_candidate() { let tracker = FakeTracker::new(vec![issue]); let state_store = StateStore::open_in_memory().expect("state store should open"); let summary = - orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("dry run should succeed"); assert!(summary.is_none(), "service-scoped queue labels should isolate intake"); @@ -482,7 +486,7 @@ fn dry_run_falls_back_to_normal_issue_when_retained_retry_loses_ownership() { ) .expect("worktree mapping should record"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("dry run should succeed") .expect("normal queued issue should be selected after retained retry is excluded"); diff --git a/apps/decodex/src/orchestrator/tests/intake/workflow_reload.rs b/apps/decodex/src/orchestrator/tests/intake/workflow_reload.rs index fbb94da..8a437af 100644 --- a/apps/decodex/src/orchestrator/tests/intake/workflow_reload.rs +++ b/apps/decodex/src/orchestrator/tests/intake/workflow_reload.rs @@ -75,6 +75,7 @@ fn configured_cycle_workflow_snapshot_overrides_invalid_disk_workflow() { dispatch_mode: IssueDispatchMode::Normal, preferred_run_identity: None, preferred_retry_budget_base: None, + allow_unverified_codex: false, }) .expect("target issue dry run should succeed with the supplied snapshot"); diff --git a/apps/decodex/src/orchestrator/tests/recovery/closeout/dispatch.rs b/apps/decodex/src/orchestrator/tests/recovery/closeout/dispatch.rs index a0f8743..f2f5212 100644 --- a/apps/decodex/src/orchestrator/tests/recovery/closeout/dispatch.rs +++ b/apps/decodex/src/orchestrator/tests/recovery/closeout/dispatch.rs @@ -59,7 +59,7 @@ fn closeout_dispatch_completes_merged_lane_without_agent_turn() { .expect("run attempt should record"); let summary = - orchestrator::execute_issue_run(&tracker, &config, &workflow, &state_store, issue_run) + orchestrator::execute_issue_run(&tracker, &config, &workflow, &state_store, issue_run, false) .expect("deterministic closeout should complete"); assert_eq!(summary.dispatch_mode, IssueDispatchMode::Closeout); @@ -121,6 +121,7 @@ fn direct_closeout_dispatch_reuses_completed_handoff_run_identity_for_record_and dispatch_mode: IssueDispatchMode::Closeout, preferred_run_identity: None, preferred_retry_budget_base: None, + allow_unverified_codex: false, }) .expect("direct retained closeout should run") .expect("closeout summary should be printed"); @@ -201,6 +202,7 @@ fn same_run_closeout_reuses_matching_active_handoff_lease() { &fixture.workflow, &fixture.state_store, &source_summary, + false, ) .expect("same-run retained closeout should run") .expect("same-run retained closeout should produce a summary"); @@ -279,7 +281,7 @@ fn closeout_dispatch_validates_pr_before_marking_issue_done() { .expect("run attempt should record"); let error = - orchestrator::execute_issue_run(&tracker, &config, &workflow, &state_store, issue_run) + orchestrator::execute_issue_run(&tracker, &config, &workflow, &state_store, issue_run, false) .expect_err("unmerged PR should stop deterministic closeout"); assert!( diff --git a/apps/decodex/src/orchestrator/tests/recovery/closeout/identity.rs b/apps/decodex/src/orchestrator/tests/recovery/closeout/identity.rs index 78b1a1e..9f9a1c4 100644 --- a/apps/decodex/src/orchestrator/tests/recovery/closeout/identity.rs +++ b/apps/decodex/src/orchestrator/tests/recovery/closeout/identity.rs @@ -11,6 +11,7 @@ fn run_project_once_closeout_reuses_completed_handoff_run_identity_for_record_an &fixture.workflow, &fixture.state_store, true, + false, ) .expect("retained closeout dry-run planning should succeed"); let planned = @@ -26,6 +27,7 @@ fn run_project_once_closeout_reuses_completed_handoff_run_identity_for_record_an &fixture.workflow, &fixture.state_store, false, + false, ) .expect("retained closeout should run") .expect("closeout summary should be printed"); @@ -125,6 +127,7 @@ fn daemon_planned_closeout_reuses_handoff_identity_after_parent_failed_status() &fixture.config, &fixture.workflow, &fixture.state_store, + ) .expect("daemon planning should succeed") .expect("retained closeout should be selected"); @@ -213,6 +216,7 @@ fn daemon_planned_closeout_allocates_retry_after_recorded_closeout_failure() { &fixture.config, &fixture.workflow, &fixture.state_store, + ) .expect("daemon planning should succeed") .expect("retained closeout should be selected"); @@ -237,6 +241,7 @@ fn run_project_once_closeout_preserves_handoff_identity_after_fresh_activity_rec &fixture.workflow, &fixture.state_store, false, + false, ) .expect("retained closeout should run after recovery") .expect("closeout summary should be printed"); diff --git a/apps/decodex/src/orchestrator/tests/recovery/runtime_reentry.rs b/apps/decodex/src/orchestrator/tests/recovery/runtime_reentry.rs index cbd831b..51df19d 100644 --- a/apps/decodex/src/orchestrator/tests/recovery/runtime_reentry.rs +++ b/apps/decodex/src/orchestrator/tests/recovery/runtime_reentry.rs @@ -181,7 +181,7 @@ fn run_project_once_prefers_recovered_in_progress_worktree_after_empty_state_sta .ensure_worktree(&issue.identifier, false) .expect("recovered worktree should be created") .path; - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("recovered dry run should succeed") .expect("active recovered issue should be selected"); @@ -256,7 +256,7 @@ fn run_project_once_recovers_retained_worktree_from_issue_identifier() { .ensure_worktree(&issue.identifier, false) .expect("recovered worktree should be created") .path; - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("recovered dry run should succeed") .expect("active recovered issue should be selected"); @@ -300,7 +300,7 @@ fn run_project_once_recovers_ready_post_review_lane_before_landing() { &sample_review_handoff_marker(&worktree.branch_name, pr_url, &head_oid), ); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false, false) .expect("recovered retained post-review lane should reconcile"); assert!( @@ -347,7 +347,7 @@ fn materialize_run_summary_worktree_creates_worktree_before_child_activity_marke vec![vec![issue.clone()], vec![issue.clone()]], ); let state_store = StateStore::open_in_memory().expect("state store should open"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("dry-run planning should succeed") .expect("brand-new lane should be selected"); @@ -465,7 +465,7 @@ fn materialize_daemon_spawn_state_uses_retained_retry_budget_marker() { state::write_run_retry_budget_attempt_count(&retained_worktree.path, "older-run", 4, 2) .expect("retry budget marker should write"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("dry-run planning should succeed") .expect("retained lane should still be selected"); let daemon_spawn_state = @@ -495,7 +495,7 @@ fn run_project_once_skips_recovered_worktree_with_fresh_activity_marker() { state::write_run_activity_marker(&worktree.path, "run-1", 1) .expect("activity marker should write"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("recovery should succeed"); assert!( @@ -557,7 +557,7 @@ fn run_project_once_retries_recovered_worktree_after_marker_process_is_killed() "kill-smoke child process should no longer be live after kill" ); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("kill-smoke recovery should succeed") .expect("killed-process recovered lane should be selected for retry"); @@ -590,7 +590,7 @@ fn run_project_once_retries_recovered_worktree_from_previous_boot() { rewrite_run_activity_marker_host_boot_id(&worktree.path, "previous-boot"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("previous-boot recovery should succeed") .expect("previous-boot recovered lane should be selected for retry"); @@ -623,7 +623,7 @@ fn run_project_once_retries_recovered_worktree_from_reused_pid() { rewrite_run_activity_marker_process_start_identity(&worktree.path, "previous-process-start"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("same-boot PID-reuse recovery should succeed") .expect("same-boot PID-reuse recovered lane should be selected for retry"); @@ -667,7 +667,7 @@ fn run_project_once_clears_recovered_lease_when_marker_turns_stale() { .expect("fresh activity marker should write"); let initial_summary = - orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("initial recovery should succeed"); assert!( @@ -686,7 +686,7 @@ fn run_project_once_clears_recovered_lease_when_marker_turns_stale() { state::write_run_activity_marker_for_process(&worktree.path, "run-1", 1, u32::MAX) .expect("stale activity marker should write"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("stale recovery should succeed") .expect("stale recovered lease should no longer block retry planning"); @@ -717,7 +717,7 @@ fn run_project_once_skips_recovered_terminal_guarded_worktree_after_empty_state_ ) .expect("terminal guard marker should write"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("recovery should succeed"); assert!( @@ -740,7 +740,7 @@ fn run_project_once_clears_terminal_queued_lane_labels_without_dispatch() { let issue = sample_issue("Done", &[active_label.as_str()]); let tracker = FakeTracker::new(vec![issue.clone()]); let state_store = StateStore::open_in_memory().expect("state store should open"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false, false) .expect("terminal queued cleanup should succeed"); assert!(summary.is_none(), "terminal queued issues should not dispatch"); @@ -760,7 +760,7 @@ fn run_project_once_dry_run_keeps_terminal_queued_lane_labels() { let issue = sample_issue("Done", &[active_label.as_str()]); let tracker = FakeTracker::new(vec![issue.clone()]); let state_store = StateStore::open_in_memory().expect("state store should open"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("terminal queued dry run should succeed"); assert!(summary.is_none(), "terminal queued dry run should not dispatch"); @@ -783,7 +783,7 @@ fn run_project_once_preserves_terminal_recovered_worktree_without_prior_state_wh let worktree = worktree_manager .ensure_worktree(&issue.identifier, false) .expect("terminal retained worktree should be created"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false, false) .expect("reconciliation should finish cleanly"); assert!( @@ -845,7 +845,7 @@ fn run_project_once_clears_stale_completed_closeout_lease_but_keeps_worktree() { ) .expect("worktree mapping should record"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false, false) .expect("startup reconciliation should succeed"); assert!( @@ -922,7 +922,7 @@ fn run_project_once_preserves_fresh_completed_closeout_lease() { ) .expect("worktree mapping should record"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false, false) .expect("startup reconciliation should succeed"); assert!( @@ -994,7 +994,7 @@ fn run_project_once_preserves_completed_unmerged_closeout_worktree() { ) .expect("worktree mapping should record"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false, false) .expect("startup reconciliation should succeed"); assert!( @@ -1040,7 +1040,7 @@ fn run_project_once_skips_recovered_worktree_without_service_active_label() { .ensure_worktree(&issue.identifier, false) .expect("foreign retained worktree should exist"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("recovery should succeed"); assert!( @@ -1083,7 +1083,7 @@ fn run_project_once_recovers_worktree_when_identifier_lookup_labels_are_truncate .ensure_worktree(&listed_issue.identifier, false) .expect("recovered worktree should be created") .path; - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("recovery should succeed") .expect("ambiguous label pagination should still recover the owned retained lane"); @@ -1144,7 +1144,7 @@ fn live_run_skips_issue_that_becomes_ineligible_after_worktree_prepare() { vec![vec![], vec![listed_issue.clone()], vec![sample_issue("In Progress", &[])]], ); let state_store = StateStore::open_in_memory().expect("state store should open"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false, false) .expect("run once should succeed"); assert!(summary.is_none()); @@ -1167,7 +1167,7 @@ fn live_run_clears_claimed_lease_when_refresh_fails_after_worktree_prepare() { let tracker = FakeTracker::with_refresh_error(vec![listed_issue.clone()], "transient refresh failure"); let state_store = StateStore::open_in_memory().expect("state store should open"); - let error = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false) + let error = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false, false) .expect_err("run once should propagate refresh failure"); assert!( @@ -1196,7 +1196,7 @@ fn run_project_once_ignores_fresh_marker_for_exited_process() { state::write_run_activity_marker_for_process(&worktree.path, "run-1", 1, exited_process_id) .expect("activity marker should write"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, true, false) .expect("recovery should succeed") .expect("dead process marker should not block retry planning"); diff --git a/apps/decodex/src/orchestrator/tests/retry/scheduling.rs b/apps/decodex/src/orchestrator/tests/retry/scheduling.rs index 0459d5a..dbfa820 100644 --- a/apps/decodex/src/orchestrator/tests/retry/scheduling.rs +++ b/apps/decodex/src/orchestrator/tests/retry/scheduling.rs @@ -98,6 +98,7 @@ fn retry_run_dry_run_enforces_active_ownership() { dispatch_mode: IssueDispatchMode::Retry, preferred_run_identity: None, preferred_retry_budget_base: None, + allow_unverified_codex: false, }) .expect("retry run should succeed"); @@ -130,6 +131,7 @@ fn targeted_run_dry_run_accepts_startable_issue_with_normal_dispatch() { dispatch_mode: IssueDispatchMode::Normal, preferred_run_identity: None, preferred_retry_budget_base: None, + allow_unverified_codex: false, }) .expect("targeted run should succeed"); @@ -166,6 +168,7 @@ fn retry_run_dry_run_rejects_terminal_guarded_issue_without_attention_label() { dispatch_mode: IssueDispatchMode::Retry, preferred_run_identity: None, preferred_retry_budget_base: None, + allow_unverified_codex: false, }) .expect("retry run should succeed"); @@ -1371,6 +1374,7 @@ fn daemon_tick_reconciles_ready_retained_review_lane_before_dry_run_planning() { review_state, )]), recoverable_worktree_skip_cache: None, + allow_unverified_codex: false, }, ); @@ -1489,6 +1493,7 @@ fn daemon_tick_clears_terminal_mapping_without_worktree_before_retained_land() { ), )]), recoverable_worktree_skip_cache: None, + allow_unverified_codex: false, }, ) .expect("daemon tick should not fail on stale terminal worktree state"); diff --git a/apps/decodex/src/orchestrator/tests/retry/selection.rs b/apps/decodex/src/orchestrator/tests/retry/selection.rs index 755a7da..1132ddd 100644 --- a/apps/decodex/src/orchestrator/tests/retry/selection.rs +++ b/apps/decodex/src/orchestrator/tests/retry/selection.rs @@ -210,6 +210,7 @@ fn blocked_future_retry_excludes_all_queued_retries_before_normal_fallback() { &config, &workflow, &state_store, + ) .expect("daemon planning should succeed") .expect("normal work should still fill open capacity"); @@ -447,6 +448,7 @@ fn due_continuation_retry_dispatches_when_issue_still_reflects_startable_state() &config, &workflow, &state_store, + ) .expect("daemon planning should succeed") .expect("the continuation retry should still dispatch"); diff --git a/apps/decodex/src/orchestrator/tests/runtime/failure.rs b/apps/decodex/src/orchestrator/tests/runtime/failure.rs index 2db99b7..4054d14 100644 --- a/apps/decodex/src/orchestrator/tests/runtime/failure.rs +++ b/apps/decodex/src/orchestrator/tests/runtime/failure.rs @@ -910,7 +910,7 @@ fn live_run_without_candidate_does_not_require_github_token_authority() { let (_temp_dir, config, workflow) = temp_project_layout(); let tracker = FakeTracker::with_refresh_snapshots_and_project(vec![], vec![vec![]], true); let state_store = StateStore::open_in_memory().expect("state store should open"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false, false) .expect("empty backlog should not require github token authority"); assert!(summary.is_none()); @@ -1025,6 +1025,7 @@ fn execute_issue_run_clears_lease_when_active_label_setup_fails() { &workflow, &state_store, issue_run.clone(), + false, ) .expect_err("active-label setup failure should abort execution"); @@ -1070,7 +1071,7 @@ fn reconciliation_clears_stale_leases_and_terminal_worktrees() { ) .expect("worktree mapping should record"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false, false) .expect("reconciliation should succeed"); assert!(summary.is_none()); @@ -1116,7 +1117,7 @@ fn reconciliation_runs_without_project_validation() { .upsert_lease("pubfi", &issue.id, "run-1", "In Progress") .expect("lease should record"); - let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false) + let summary = orchestrator::run_project_once(&tracker, &config, &workflow, &state_store, false, false) .expect("reconciliation should still succeed without any project validation"); assert!(summary.is_none(), "reconciliation-only startup should not dispatch a new lane here"); diff --git a/apps/decodex/src/orchestrator/types.rs b/apps/decodex/src/orchestrator/types.rs index c9d6178..1810970 100644 --- a/apps/decodex/src/orchestrator/types.rs +++ b/apps/decodex/src/orchestrator/types.rs @@ -25,6 +25,7 @@ pub(crate) struct RunOnceRequest<'a> { pub(crate) preferred_attempt_number: Option, pub(crate) preferred_retry_budget_base: Option, pub(crate) preferred_workflow_snapshot: Option<&'a str>, + pub(crate) allow_unverified_codex: bool, } /// Multi-project local control-plane daemon request. @@ -32,6 +33,7 @@ pub(crate) struct ServeRequest<'a> { pub(crate) config_path: Option<&'a Path>, pub(crate) listen_address: &'a str, pub(crate) dev: bool, + pub(crate) allow_unverified_codex: bool, } /// Agent-readable runtime diagnosis request. @@ -145,6 +147,7 @@ struct RunCycleRequest<'a> { preferred_run_identity: Option>, preferred_retry_budget_base: Option, preferred_workflow_snapshot: Option<&'a str>, + allow_unverified_codex: bool, } struct SpawnRunOnceChildRequest<'a> { @@ -157,6 +160,7 @@ struct SpawnRunOnceChildRequest<'a> { preferred_attempt_number: i64, preferred_retry_budget_base: i64, workflow: &'a WorkflowDocument, + allow_unverified_codex: bool, issue_claim_handoff: Option<&'a File>, dispatch_slot_handoff: Option<&'a File>, dispatch_slot_index_handoff: Option, @@ -1191,6 +1195,7 @@ struct TargetIssueRunContext<'a, T> { dispatch_mode: IssueDispatchMode, preferred_run_identity: Option>, preferred_retry_budget_base: Option, + allow_unverified_codex: bool, } struct ConcurrencySnapshot { diff --git a/apps/decodex/src/radar.rs b/apps/decodex/src/radar.rs index e384494..047a57d 100644 --- a/apps/decodex/src/radar.rs +++ b/apps/decodex/src/radar.rs @@ -924,8 +924,9 @@ impl GitHubError { fn into_report(self, url: &str) -> Report { match self { - Self::Status { status, body } => - eyre::eyre!("GitHub API request failed for {url}: {} {body}", status.as_u16()), + Self::Status { status, body } => { + eyre::eyre!("GitHub API request failed for {url}: {} {body}", status.as_u16()) + }, Self::Transport(error) => eyre::eyre!("GitHub API request failed for {url}: {error}"), } } diff --git a/apps/decodex/src/tracker/linear.rs b/apps/decodex/src/tracker/linear.rs index 54bbc1e..bffaf8b 100644 --- a/apps/decodex/src/tracker/linear.rs +++ b/apps/decodex/src/tracker/linear.rs @@ -890,8 +890,9 @@ fn rate_limited_error_message(errors: &[GraphqlError]) -> Option { let reset = extensions.get("reset").and_then(Value::as_i64); Some(match reset { - Some(reset) => - format!("Linear connector is rate limited until `{reset}`: {user_message}"), + Some(reset) => { + format!("Linear connector is rate limited until `{reset}`: {user_message}") + }, None => format!("Linear connector is rate limited: {user_message}"), }) }) diff --git a/docs/spec/app-server.md b/docs/spec/app-server.md index a5dc7b9..203c6dc 100644 --- a/docs/spec/app-server.md +++ b/docs/spec/app-server.md @@ -83,6 +83,11 @@ compatibility evidence, not the current upgrade target. runs after the bounded capability preflight and before `thread/start` or `thread/resume`; an app-server identity outside the locally verified list is a pre-dispatch app-server preflight blocker rather than a promptable agent turn. +Operators may pass `--allow-unverified-codex` to `decodex run`, `decodex serve`, or +`decodex probe` when deliberately dogfooding a development Codex build. This changes +only the unsupported compatibility identity from a blocker to a warning with +`compatibility=unverified_allowed`; all other capability preflight blockers remain +fail-closed. Current upstream Codex signals are beyond the local support claim whenever they are newer than the latest locally probed version, or when checked-in Radar queue entries diff --git a/docs/spec/runtime.md b/docs/spec/runtime.md index 32430a7..7c451be 100644 --- a/docs/spec/runtime.md +++ b/docs/spec/runtime.md @@ -506,6 +506,8 @@ After a process restart, recent-run history, active lease ownership, retained po [`app-server.md`](./app-server.md). Missing config/model/provider/skills/plugin/MCP state, or an app-server identity outside the locally verified compatibility range, is a pre-dispatch terminal blocker with an operator-readable error class, not a - promptable agent turn. + promptable agent turn, unless the operator explicitly started `run`, `serve`, or + `probe` with `--allow-unverified-codex`. That override downgrades only the unverified + compatibility identity to a warning; capability failures still block dispatch. - If the local process crashed during a run, `decodex` must recover from the runtime database, current tracker cache or state, and retained worktree inspection. - If Linear shows a non-terminal state but no local lease exists, the issue may become eligible again after reconciliation or may be redispatched through the retained recovered worktree. diff --git a/plugins/decodex/skills/manual-cli/SKILL.md b/plugins/decodex/skills/manual-cli/SKILL.md index 13c7b27..52c52c3 100644 --- a/plugins/decodex/skills/manual-cli/SKILL.md +++ b/plugins/decodex/skills/manual-cli/SKILL.md @@ -131,6 +131,9 @@ Manual commit and landing are separate narrow workflows: - Use `run --dry-run` before live automation to validate project loading, issue discovery, eligibility, and worktree planning without tracker mutation. - Use `probe stdio://` before relying on the Codex app-server boundary. +- Use `--allow-unverified-codex` on `run`, `serve`, or `probe` only for deliberate + Codex development-version dogfooding. It turns an unsupported app-server identity + into a warning while keeping other preflight failures blocking. - Treat hidden `serve --dev` as isolated local-development infrastructure only. It serves dashboard, account, and app snapshot APIs, but it does not register projects, poll Linear, dispatch work, or accept `--config`. Decodex App's fallback server uses diff --git a/scripts/README.md b/scripts/README.md index 529f0c7..c92f528 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -5,6 +5,8 @@ This directory contains executable repository automation helpers. - `scripts/github/` owns the automation-only Codex AI analysis helper and shared schema support used by that helper. - `scripts/config/` owns config-derived artifact synchronization scripts. +- `scripts/macos/` owns macOS-only app packaging and local bundle verification + helpers. Checked-in data produced or consumed by scripts belongs outside this directory. GitHub review queues, upstream reviews, bundles, impact records, and analysis drafts live diff --git a/scripts/macos/test_decodex_app_stage.sh b/scripts/macos/test_decodex_app_stage.sh new file mode 100755 index 0000000..1129760 --- /dev/null +++ b/scripts/macos/test_decodex_app_stage.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +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_details="$(codesign -dv --verbose=4 "$app_path" 2>&1)" +grep -q '^TeamIdentifier=' <<<"$codesign_details" +grep -q 'flags=.*runtime' <<<"$codesign_details" + +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' From 1a0fd1737f6a673b4750aa447afd4b7095530184 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Wed, 3 Jun 2026 12:15:51 +0800 Subject: [PATCH 3/3] {"schema":"decodex/commit/1","summary":"Fix vstyle entrypoint split","authority":"manual"} --- apps/decodex/src/orchestrator/entrypoints.rs | 76 +++++++++++--------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/apps/decodex/src/orchestrator/entrypoints.rs b/apps/decodex/src/orchestrator/entrypoints.rs index fc20184..798432f 100644 --- a/apps/decodex/src/orchestrator/entrypoints.rs +++ b/apps/decodex/src/orchestrator/entrypoints.rs @@ -74,41 +74,13 @@ pub(crate) fn run_once(request: RunOnceRequest<'_>) -> Result<()> { }; if request.explain_queue { - if !request.dry_run { - eyre::bail!("queue explanation is only supported for dry-run execution."); - } - if request.preferred_issue_id.is_some() { - eyre::bail!("queue explanation does not accept a preferred issue."); - } - - let tracker = LinearClient::new(config.tracker().resolve_api_key()?)?; - let queued_candidates = match build_queued_candidate_statuses( - &tracker, + return run_queue_explain( &config, &workflow, &state_store, - ) { - Ok(queued_candidates) => queued_candidates, - Err(error) => { - let Some(backoff) = - tracker_rate_limit_backoff(&error, Instant::now(), "queue_explain") - else { - return Err(error); - }; - let status = backoff - .to_operator_status(config.service_id(), OffsetDateTime::now_utc().unix_timestamp()); - - persist_tracker_backoff_state(&state_store, config.service_id(), &backoff); - - print!("{}", render_tracker_backoff_cli_message("run", &status)); - - return Ok(()); - }, - }; - - print!("{}", render_queue_explain(&config, &queued_candidates)); - - return Ok(()); + request.dry_run, + request.preferred_issue_id, + ); } let run_summary = match run_configured_cycle(RunCycleRequest { @@ -468,6 +440,46 @@ pub(crate) fn print_private_evidence(request: EvidenceRequest<'_>) -> Result<()> Ok(()) } +fn run_queue_explain( + config: &ServiceConfig, + workflow: &WorkflowDocument, + state_store: &StateStore, + dry_run: bool, + preferred_issue_id: Option<&str>, +) -> Result<()> { + if !dry_run { + eyre::bail!("queue explanation is only supported for dry-run execution."); + } + if preferred_issue_id.is_some() { + eyre::bail!("queue explanation does not accept a preferred issue."); + } + + let tracker = LinearClient::new(config.tracker().resolve_api_key()?)?; + let queued_candidates = + match build_queued_candidate_statuses(&tracker, config, workflow, state_store) { + Ok(queued_candidates) => queued_candidates, + Err(error) => { + let Some(backoff) = + tracker_rate_limit_backoff(&error, Instant::now(), "queue_explain") + else { + return Err(error); + }; + let status = + backoff.to_operator_status(config.service_id(), OffsetDateTime::now_utc().unix_timestamp()); + + persist_tracker_backoff_state(state_store, config.service_id(), &backoff); + + print!("{}", render_tracker_backoff_cli_message("run", &status)); + + return Ok(()); + }, + }; + + print!("{}", render_queue_explain(config, &queued_candidates)); + + Ok(()) +} + fn print_operator_status_snapshot( snapshot: &OperatorStatusSnapshot, json: bool,