From d59ec0b258d10d19501adee34882b15ff408aaba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 14 Sep 2026 03:22:21 +0000 Subject: [PATCH] chore: sync public mirror from internal --- CHANGELOG.md | 23 +++ Cargo.lock | 2 +- docs/MODELS.md | 4 +- .../codex-subagent-dispatch-table-v1.json | 12 +- package-lock.json | 4 +- package.json | 2 +- packages/ai-rs/src/model_capabilities.rs | 22 ++- packages/ai-rs/src/openai.rs | 106 ++++++++++- packages/ambient-agent-rs/src/pr_creator.rs | 8 +- packages/local-host-rs/src/agent/mod.rs | 26 ++- .../src/agent/native_admission_tests.rs | 61 ++++++ packages/local-host-rs/src/codex_cli.rs | 42 ++--- packages/local-host-rs/src/config.rs | 10 +- packages/local-host-rs/src/credential_mode.rs | 173 ++++++++++++------ packages/local-host-rs/src/doctor.rs | 15 +- .../src/headless/remote_transport/tests.rs | 30 +-- .../src/headless/supervisor/tests.rs | 30 +-- packages/local-host-rs/src/headless_server.rs | 153 +++++++++++++++- packages/local-host-rs/src/hosted_runner.rs | 11 +- .../local-host-rs/src/hosted_runner/tests.rs | 20 +- .../hosted_runner/tests/transport_flush.rs | 90 +++++++++ packages/local-host-rs/src/model_catalog.rs | 91 ++++++--- .../local-host-rs/src/model_catalog_data.json | 70 +++---- packages/maestro-rs/Cargo.toml | 2 +- packages/runtime-gateway-rs/src/a2a/mod.rs | 3 +- packages/runtime-gateway-rs/src/a2a/tasks.rs | 89 ++++++++- .../runtime-gateway-rs/src/model_catalog.rs | 10 +- packages/runtime-gateway-rs/src/tests.rs | 76 +++++++- packages/runtime-rs/src/agent/native/codex.rs | 87 ++++++++- packages/runtime-rs/src/agent/retry.rs | 21 +++ packages/swarm-rs/src/types.rs | 12 +- packages/swarm-rs/src/types/tests.rs | 2 +- .../docs/user-guide/02-authentication.md | 4 +- packages/tui-rs/src/app/input_handlers.rs | 2 +- packages/tui-rs/src/components/message.rs | 32 ++-- .../tui-rs/src/components/model_selector.rs | 82 ++++++++- packages/tui-rs/src/onboarding_checks.rs | 2 +- packages/ui-rs/src/translations.rs | 11 ++ .../hosted-orb-delegation-acceptance.test.mjs | 22 --- 39 files changed, 1156 insertions(+), 306 deletions(-) create mode 100644 packages/local-host-rs/src/hosted_runner/tests/transport_flush.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 420330d62..7cc0cb12a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,29 @@ versioning when releases are cut. and keep scheduled public runs inert so public publishing stays downstream of the internal source-of-truth release. +## [0.10.88] - 2026-09-13 + +### Added + +- Managed DeepSeek V4.1 Flash, Qwen 3.8 Max, and Gemini 3.8 Flash model choices. + Availability requires an enabled gateway route, provider enrollment, and model credits. + GLM-5.3 remains the default; managed requests support text and tools. + +### Improved + +- Show the upstream provider and “Model credits” in the model picker so managed + routes are distinct from routes that use your own provider keys. Selection + still uses the exact managed route; the picker does not claim live availability. + +### Fixed + +- Include Gemini thinking tokens in gateway output charges and require final + native usage before settling a streamed request. +- Preserve each managed model's provider and credential through helper calls and continuation. +- Stop retrying exhausted provider quotas reported as HTTP 429. Insufficient + managed credits also produce a terminal quota result; temporary rate limits + retain their existing retry behavior. + ## [0.10.86] - 2026-09-11 ### Added diff --git a/Cargo.lock b/Cargo.lock index 60acc936e..7f89f7f74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3509,7 +3509,7 @@ checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" [[package]] name = "maestro" -version = "0.10.86" +version = "0.10.88" dependencies = [ "anyhow", "ctor", diff --git a/docs/MODELS.md b/docs/MODELS.md index 4a474207e..c0cd74daf 100644 --- a/docs/MODELS.md +++ b/docs/MODELS.md @@ -332,8 +332,8 @@ generator, so you can use them out of the box: `https://api.groq.com/openai/v1/responses`. - **OpenAI Codex (Codex app-server + ChatGPT sign-in):** `gpt-5.1`, `gpt-5.2`, `gpt-5.2-codex`, `gpt-5.3-codex`, - `gpt-5.3-codex-spark`, `gpt-5.4`, `gpt-5.4-mini`, and `gpt-5.5` - (the live-catalog default for OpenAI) under the `openai-codex` + `gpt-5.3-codex-spark`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, + and `gpt-5.6` (the live-catalog default for OpenAI) under the `openai-codex` provider. These use `api: "openai-codex-app-server"` and require `maestro codex login` to Sign in with ChatGPT through Codex app-server. Published Maestro installs use the packaged `@openai/codex` app-server first diff --git a/docs/protocols/codex-subagent-dispatch-table-v1.json b/docs/protocols/codex-subagent-dispatch-table-v1.json index 1074c9167..0d8f99312 100644 --- a/docs/protocols/codex-subagent-dispatch-table-v1.json +++ b/docs/protocols/codex-subagent-dispatch-table-v1.json @@ -7,19 +7,19 @@ "opus": { "anthropic": "claude-opus-4-6", "openai": "gpt-5.2", - "openai-codex": "gpt-5.5", + "openai-codex": "gpt-5.6", "google": "gemini-2.0-flash-thinking-exp" }, "sonnet": { "anthropic": "claude-sonnet-4-5-20250929", "openai": "gpt-4o", - "openai-codex": "gpt-5.4", + "openai-codex": "gpt-5.6", "google": "gemini-2.0-flash-exp" }, "haiku": { "anthropic": "claude-haiku-4-5-20251001", "openai": "gpt-4o-mini", - "openai-codex": "gpt-5.4-mini", + "openai-codex": "gpt-5.6", "google": "gemini-2.0-flash-lite-exp" } }, @@ -139,7 +139,7 @@ "explorer": { "model": "haiku", "reasoningEffort": "low" }, "planner": { "model": "opus", "reasoningEffort": "high" }, "coder": { - "model": "openai-codex/gpt-5.5", + "model": "openai-codex/gpt-5.6", "reasoningEffort": "medium" }, "reviewer": { "model": "sonnet", "reasoningEffort": "medium" }, @@ -190,11 +190,11 @@ "explorer": { "model": "sonnet", "reasoningEffort": "medium" }, "planner": { "model": "opus", "reasoningEffort": "xhigh" }, "coder": { - "model": "openai-codex/gpt-5.5", + "model": "openai-codex/gpt-5.6", "reasoningEffort": "high" }, "reviewer": { - "model": "openai-codex/gpt-5.5", + "model": "openai-codex/gpt-5.6", "reasoningEffort": "medium" }, "researcher": { "model": "sonnet", "reasoningEffort": "medium" }, diff --git a/package-lock.json b/package-lock.json index 20d4da564..d7f297cc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@evalops/maestro", - "version": "0.10.86", + "version": "0.10.88", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@evalops/maestro", - "version": "0.10.86", + "version": "0.10.88", "license": "BUSL-1.1", "bin": { "deixic-code": "bin/deixic-code", diff --git a/package.json b/package.json index 23aa08eb2..b74f88f84 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@evalops/deixic-code", "description": "Deixic Code — native Rust coding agent, CLI, TUI, and web runtime gateway", - "version": "0.10.86", + "version": "0.10.88", "private": false, "type": "module", "bin": { diff --git a/packages/ai-rs/src/model_capabilities.rs b/packages/ai-rs/src/model_capabilities.rs index 8293d2477..b1efdafea 100644 --- a/packages/ai-rs/src/model_capabilities.rs +++ b/packages/ai-rs/src/model_capabilities.rs @@ -205,15 +205,16 @@ pub fn openai_request_capabilities( let name = stripped.rsplit('/').next().unwrap_or(stripped); if name.starts_with("gpt-5.6-") { "max" - } else if ["gpt-5.2", "gpt-5.3", "gpt-5.4", "gpt-5.5"] - .iter() - .any(|prefix| { - name == *prefix - || name - .strip_prefix(prefix) - .is_some_and(|rest| rest.starts_with("-")) - }) - { + } else if [ + "gpt-5.2", "gpt-5.3", "gpt-5.4", "gpt-5.5", "gpt-5.6", "gpt-5.7", + ] + .iter() + .any(|prefix| { + name == *prefix + || name + .strip_prefix(prefix) + .is_some_and(|rest| rest.starts_with("-")) + }) { "xhigh" } else { "high" @@ -323,7 +324,10 @@ mod tests { #[test] fn maximum_effort_is_provider_and_model_specific() { for (model, expected) in [ + ("gpt-5.4", "xhigh"), ("gpt-5.5", "xhigh"), + ("gpt-5.6", "xhigh"), + ("gpt-5.7", "xhigh"), ("gpt-5.6-luna", "max"), ("unknown", "high"), ] { diff --git a/packages/ai-rs/src/openai.rs b/packages/ai-rs/src/openai.rs index 59534b371..1f2b487cf 100644 --- a/packages/ai-rs/src/openai.rs +++ b/packages/ai-rs/src/openai.rs @@ -589,6 +589,16 @@ fn responses_item_id(id: &str) -> Option<&str> { id.split_once('|').map(|(_, item_id)| item_id) } +// These are the Google-protocol provider IDs and aliases accepted by the +// model-provider-core catalog. This classifies opaque replay metadata only; +// signed route validation and provider selection remain owned by the gateway. +fn supports_gemini_tool_history(provider: Option<&str>) -> bool { + matches!( + provider, + Some("google" | "googleai" | "google-ai" | "gemini" | "vertex-ai" | "vertex_ai") + ) +} + /// Extract function call from a `ResponseItem` /// /// # Pattern: Option Chaining @@ -1375,6 +1385,37 @@ impl OpenAiClient { .context("managed gateway request body must be an object")?; if let Some((authorization, _)) = authorization { project_managed_inference_route(object, &authorization)?; + // Provider history follows the signed route, not the legacy + // constructor's provider. Keep native Gemini context only when + // the admitted route can replay it; it is never tool authority. + let gemini_route = object + .get("provider_candidates") + .and_then(serde_json::Value::as_array) + .is_some_and(|candidates| { + candidates.iter().any(|candidate| { + supports_gemini_tool_history( + candidate + .pointer("/provider_ref/provider") + .and_then(serde_json::Value::as_str), + ) + }) + }); + if !gemini_route { + if let Some(input) = object + .get_mut("input") + .and_then(serde_json::Value::as_array_mut) + { + for item in input { + if item.get("type").and_then(serde_json::Value::as_str) + == Some("function_call") + { + if let Some(item) = item.as_object_mut() { + item.remove("extra_content"); + } + } + } + } + } object.insert("managed_inference_authorization".to_string(), authorization); } if let Some(context) = managed_context { @@ -1891,13 +1932,13 @@ impl OpenAiClient { function_call["id"] = serde_json::json!(item_id); } if self.managed_gateway - && matches!( - self.request_extensions - .get("provider_ref") - .and_then(|provider| provider.get("provider")) - .and_then(serde_json::Value::as_str), - Some("gemini" | "vertex_ai") - ) + && (self.managed_inference_authorization.is_some() + || supports_gemini_tool_history( + self.request_extensions + .get("provider_ref") + .and_then(|provider| provider.get("provider")) + .and_then(serde_json::Value::as_str), + )) { if let Some(context) = gemini_context { function_call["extra_content"] = @@ -4764,7 +4805,7 @@ data: {"type":"response.completed","response":{"output":[{"type":"message","cont "openrouter/anthropic/claude-sonnet-4.5", "openrouter/google/gemini-2.5-pro", "openrouter/meta-llama/llama-4-maverick", - "openrouter/openai/gpt-5.4", + "openrouter/openai/gpt-5.5", "openrouter/openai/o3-mini", "openrouter/openrouter/auto", "evalops/openrouter/gpt-5.6", @@ -5418,7 +5459,15 @@ data: {"type":"response.completed","response":{"output":[{"type":"message","cont model: "gemini-3.6-flash".into(), ..Default::default() }; - for provider in ["gemini", "vertex_ai", "openai"] { + for provider in [ + "vertex-ai", + "vertex_ai", + "gemini", + "google", + "google-ai", + "googleai", + "openai", + ] { let client = OpenAiClient::new("test-key") .unwrap() .with_managed_gateway_context( @@ -5436,6 +5485,45 @@ data: {"type":"response.completed","response":{"output":[{"type":"message","cont assert_eq!(body["input"][0]["call_id"], body["input"][1]["call_id"]); assert_eq!(body["input"][1]["output"], "wrote 6 bytes\n"); } + // Hosted turns select the provider from signed candidates. The + // legacy constructor route can name a different provider. + for provider in [ + "vertex-ai", + "vertex_ai", + "gemini", + "google", + "google-ai", + "googleai", + "openai", + ] { + let mut client = OpenAiClient::new("test-key") + .unwrap() + .with_managed_gateway_context( + "org_123", + "workspace_456", + serde_json::json!({"provider": if provider == "openai" { "vertex_ai" } else { "openrouter" }}), + ) + .unwrap(); + let mut authorization: serde_json::Value = + serde_json::from_str(&managed_authorization_fixture("gemini-history")).unwrap(); + authorization["claims"]["providerCandidates"][0]["provider"] = + serde_json::json!(provider); + client.set_managed_inference_authorization(Some(authorization.to_string())); + let body = client + .managed_request(client.build_responses_request_body(&restored, &config)) + .unwrap(); + assert!(body.get("provider_ref").is_none()); + assert_eq!( + body["provider_candidates"][0]["provider_ref"]["provider"], + provider + ); + if provider == "openai" { + assert!(body["input"][0].get("extra_content").is_none()); + } else { + assert_eq!(body["input"][0]["extra_content"], item["extra_content"]); + } + assert_eq!(body["input"][0]["call_id"], body["input"][1]["call_id"]); + } let other = OpenAiClient::new("test-key") .unwrap() .build_responses_request_body(&restored, &config); diff --git a/packages/ambient-agent-rs/src/pr_creator.rs b/packages/ambient-agent-rs/src/pr_creator.rs index f6160d6cf..bc1d8a6aa 100644 --- a/packages/ambient-agent-rs/src/pr_creator.rs +++ b/packages/ambient-agent-rs/src/pr_creator.rs @@ -742,11 +742,11 @@ mod tests { #[test] fn builds_explicit_follow_up_approval_trailer_when_missing() { let metadata = - PrCreator::build_authorship_metadata(&event_with_extra(HashMap::new()), "gpt-5.4") + PrCreator::build_authorship_metadata(&event_with_extra(HashMap::new()), "gpt-5.6") .unwrap(); assert_eq!(metadata.prompt_id, "prompt_123"); - assert_eq!(metadata.model_identifier, "gpt-5.4"); + assert_eq!(metadata.model_identifier, "gpt-5.6"); assert_eq!( metadata.approvals_id, "missing:approval-follow-up-required:prompt_123" @@ -757,7 +757,7 @@ mod tests { fn fallback_approval_trailer_uses_trimmed_prompt_id() { let mut event = event_with_extra(HashMap::new()); event.id = " prompt_123 ".to_string(); - let metadata = PrCreator::build_authorship_metadata(&event, "gpt-5.4").unwrap(); + let metadata = PrCreator::build_authorship_metadata(&event, "gpt-5.6").unwrap(); assert_eq!(metadata.prompt_id, "prompt_123"); assert_eq!( @@ -775,7 +775,7 @@ mod tests { ); let metadata = - PrCreator::build_authorship_metadata(&event_with_extra(extra), "gpt-5.4").unwrap(); + PrCreator::build_authorship_metadata(&event_with_extra(extra), "gpt-5.6").unwrap(); assert_eq!(metadata.approvals_id, "approval_789"); } diff --git a/packages/local-host-rs/src/agent/mod.rs b/packages/local-host-rs/src/agent/mod.rs index 24769685f..18c2628cb 100644 --- a/packages/local-host-rs/src/agent/mod.rs +++ b/packages/local-host-rs/src/agent/mod.rs @@ -522,10 +522,12 @@ fn resolve_native_client( } let (credential_mode, identity) = crate::credential_mode::require_ready_with_identity(model)?; - let identity_scope = crate::telemetry::TelemetryIdentityScope::new( - &identity.organization_id, - identity.workspace_id.as_deref(), - ); + let identity_scope = identity.as_ref().and_then(|identity| { + crate::telemetry::TelemetryIdentityScope::new( + &identity.organization_id, + identity.workspace_id.as_deref(), + ) + }); if let crate::codex_auth::CodexModelRoute::AppServer { model_id } = route { return Ok(( NativeResolvedClient { @@ -738,14 +740,15 @@ fn relay_runtime_events( } /// Bind a model transition before publishing its identity to turn telemetry. -/// Automatic routing cannot replace the actor's active tenant or proceed -/// without a complete verified tenant; explicit model changes refresh it. +/// Automatic routing cannot replace the actor's active tenant. Direct-provider +/// BYOK may have no tenant; staying unbound is not a tenant change. Explicit +/// model changes refresh the scope. fn update_model_identity_scope( active: &mut Option, resolved: Option, preserve_scope: bool, ) -> Result<(), String> { - if preserve_scope && (resolved.is_none() || resolved != *active) { + if preserve_scope && resolved != *active { return Err( "Automatic routing cannot change the active organization or workspace".to_owned(), ); @@ -813,8 +816,15 @@ mod identity_transition_tests { assert_eq!(active, initial); } let mut unbound = None; - assert!(update_model_identity_scope(&mut unbound, None, true).is_err()); + update_model_identity_scope(&mut unbound, None, true).unwrap(); assert!(unbound.is_none()); + let mut still_unbound = None; + assert_eq!( + update_model_identity_scope(&mut still_unbound, scope("org-a", "workspace-a"), true) + .unwrap_err(), + "Automatic routing cannot change the active organization or workspace" + ); + assert!(still_unbound.is_none()); } #[test] diff --git a/packages/local-host-rs/src/agent/native_admission_tests.rs b/packages/local-host-rs/src/agent/native_admission_tests.rs index b07c9c8b9..cdccece5a 100644 --- a/packages/local-host-rs/src/agent/native_admission_tests.rs +++ b/packages/local-host-rs/src/agent/native_admission_tests.rs @@ -132,6 +132,67 @@ fn codex_models_require_evalops_identity_before_using_the_app_server() { assert!(error.to_string().contains("deixic-code evalops login")); } +#[test] +fn openai_api_key_resolves_native_client_without_identity() { + let _guard = crate::config::test_process_env_lock(); + let mut names = identity_env_names(false, true); + names.extend(["OPENAI_API_KEY", "OPENAI_BASE_URL"]); + let _restore = EnvRestore::capture(&names); + let maestro_home = tempfile::tempdir().expect("maestro home"); + std::env::set_var("MAESTRO_HOME", maestro_home.path()); + std::env::set_var("MAESTRO_OAUTH_STORAGE_MODE", "file"); + std::env::set_var("MAESTRO_DISABLE_KEYCHAIN", "1"); + std::env::set_var("OPENAI_API_KEY", "synthetic-maestro-smoke-key"); + std::env::set_var("OPENAI_BASE_URL", "http://127.0.0.1:9/v1"); + for name in [ + crate::credential_mode::ACCESS_TOKEN_ENV, + crate::credential_mode::ACCESS_TOKEN_FILE_ENV, + crate::credential_mode::ORG_ID_ENV, + crate::credential_mode::WORKSPACE_ID_ENV, + "MAESTRO_IDENTITY_URL", + crate::init_cli::TEST_IDENTITY_AUTHORITY_ENV, + ] { + std::env::remove_var(name); + } + + let (resolved, telemetry_scope) = + super::resolve_native_client("openai/gpt-5.4", None).expect("direct OpenAI client"); + assert_eq!(resolved.provider_name, "openai"); + assert!(!resolved.model_route.uses_app_server()); + assert!(resolved.client.is_some()); + assert!(telemetry_scope.is_none()); +} + +#[test] +fn evalops_route_requires_identity_even_with_openai_api_key() { + let _guard = crate::config::test_process_env_lock(); + let mut names = identity_env_names(false, true); + names.extend(["OPENAI_API_KEY", "OPENAI_BASE_URL"]); + let _restore = EnvRestore::capture(&names); + let maestro_home = tempfile::tempdir().expect("maestro home"); + std::env::set_var("MAESTRO_HOME", maestro_home.path()); + std::env::set_var("MAESTRO_OAUTH_STORAGE_MODE", "file"); + std::env::set_var("MAESTRO_DISABLE_KEYCHAIN", "1"); + std::env::set_var("OPENAI_API_KEY", "synthetic-maestro-smoke-key"); + std::env::set_var("OPENAI_BASE_URL", "http://127.0.0.1:9/v1"); + for name in [ + crate::credential_mode::ACCESS_TOKEN_ENV, + crate::credential_mode::ACCESS_TOKEN_FILE_ENV, + crate::credential_mode::ORG_ID_ENV, + crate::credential_mode::WORKSPACE_ID_ENV, + "MAESTRO_IDENTITY_URL", + crate::init_cli::TEST_IDENTITY_AUTHORITY_ENV, + ] { + std::env::remove_var(name); + } + + let error = match super::resolve_native_client("evalops/gpt-5.5", None) { + Ok(_) => panic!("managed evalops must not bypass EvalOps Identity"), + Err(error) => error, + }; + assert!(error.to_string().contains("deixic-code evalops login")); +} + #[test] fn injected_network_client_requires_evalops_identity() { let _guard = crate::config::test_process_env_lock(); diff --git a/packages/local-host-rs/src/codex_cli.rs b/packages/local-host-rs/src/codex_cli.rs index 3e661fe2a..7cd3bbf82 100644 --- a/packages/local-host-rs/src/codex_cli.rs +++ b/packages/local-host-rs/src/codex_cli.rs @@ -617,7 +617,7 @@ async fn login_with_client( println!( "{}", crate::localization::cli_locale().format( - "Select provider \"openai-codex\" or a model like \"openai-codex/gpt-5.5\".", + "Select provider \"openai-codex\" or a model like \"openai-codex/gpt-5.6\".", &[] ) ); @@ -635,7 +635,7 @@ async fn login_with_client( println!( "{}", crate::localization::cli_locale().format( - "Select provider \"openai-codex\" or a model like \"openai-codex/gpt-5.5\".", + "Select provider \"openai-codex\" or a model like \"openai-codex/gpt-5.6\".", &[] ) ); @@ -663,7 +663,7 @@ async fn login_with_client( println!( "{}", crate::localization::cli_locale().format( - "Select provider \"openai-codex\" or a model like \"openai-codex/gpt-5.5\".", + "Select provider \"openai-codex\" or a model like \"openai-codex/gpt-5.6\".", &[] ) ); @@ -1704,13 +1704,13 @@ mod tests { "--profile".to_owned(), "work".to_owned(), "--model".to_owned(), - "openai-codex/gpt-5.4".to_owned(), + "openai-codex/gpt-5.6".to_owned(), "--json".to_owned(), ]) .unwrap(); assert_eq!(options.profile.as_deref(), Some("work")); - assert_eq!(options.model.as_deref(), Some("openai-codex/gpt-5.4")); + assert_eq!(options.model.as_deref(), Some("openai-codex/gpt-5.6")); assert!(options.json); } @@ -1719,9 +1719,9 @@ mod tests { let result = run_codex(&[ "ready".to_owned(), "--model".to_owned(), - "openai-codex/gpt-5.4".to_owned(), + "openai-codex/gpt-5.6".to_owned(), "--model".to_owned(), - "openai-codex/gpt-5.5".to_owned(), + "openai-codex/gpt-5.6".to_owned(), ]) .await; @@ -1745,7 +1745,7 @@ mod tests { let key = crate::codex_session::CodexSessionKey::new( "work", workspace.path(), - "openai-codex/gpt-5.4", + "openai-codex/gpt-5.6", )?; crate::codex_session::CodexThreadBinding::new(key, "thread-model-54", None, 1) .store_at(state_root.path())?; @@ -1756,7 +1756,7 @@ mod tests { &identity, state_root.path(), workspace.path(), - "openai-codex/gpt-5.4", + "openai-codex/gpt-5.6", &resume_unsupported_compatibility(), &json!({ "protocolVersion": "2025-01-01" }), ) @@ -1782,7 +1782,7 @@ mod tests { &identity, state_root.path(), workspace.path(), - "gpt-5.4", + "gpt-5.6", &resume_unsupported_compatibility(), &json!({ "protocolVersion": "2025-01-01" }), ) @@ -1790,7 +1790,7 @@ mod tests { assert_eq!(absent.state, "ready"); assert_eq!(absent.detail, "no binding yet"); - let key = crate::codex_session::CodexSessionKey::new("work", workspace.path(), "gpt-5.4")?; + let key = crate::codex_session::CodexSessionKey::new("work", workspace.path(), "gpt-5.6")?; let path = crate::codex_session::CodexThreadBinding::path_for_key_at(state_root.path(), &key); fs::create_dir_all(path.parent().unwrap())?; @@ -1801,7 +1801,7 @@ mod tests { &identity, state_root.path(), workspace.path(), - "gpt-5.4", + "gpt-5.6", &resume_unsupported_compatibility(), &json!({ "protocolVersion": "2025-01-01" }), ) @@ -1824,14 +1824,14 @@ mod tests { let key = crate::codex_session::CodexSessionKey::new( "work", workspace.path(), - "openai-codex/gpt-5.5", + "openai-codex/gpt-5.6", ) .unwrap(); crate::codex_session::CodexThreadBinding::new(key, "thread-remote", None, 1) .store_at(state_root.path()) .unwrap(); let options = CodexReadinessOptions { - model: "openai-codex/gpt-5.5".to_owned(), + model: "openai-codex/gpt-5.6".to_owned(), cwd: workspace.path().to_path_buf(), state_root: state_root.path().to_path_buf(), }; @@ -1844,7 +1844,7 @@ mod tests { let resume = mock.next_request().await.expect("resume validation"); assert_eq!(resume["method"], "thread/resume"); assert_eq!(resume["params"]["threadId"], "thread-remote"); - assert_eq!(resume["params"]["model"], "gpt-5.5"); + assert_eq!(resume["params"]["model"], "gpt-5.6"); mock.respond( resume["id"].as_u64().unwrap(), json!({ "thread": { "id": "thread-remote" } }), @@ -1866,13 +1866,13 @@ mod tests { codex_home: codex_home.path().to_path_buf(), workspace_boundary: None, }; - let key = crate::codex_session::CodexSessionKey::new("work", workspace.path(), "gpt-5.5") + let key = crate::codex_session::CodexSessionKey::new("work", workspace.path(), "gpt-5.6") .unwrap(); crate::codex_session::CodexThreadBinding::new(key.clone(), "missing-thread", None, 1) .store_at(state_root.path()) .unwrap(); let options = CodexReadinessOptions { - model: "openai-codex/gpt-5.5".to_owned(), + model: "openai-codex/gpt-5.6".to_owned(), cwd: workspace.path().to_path_buf(), state_root: state_root.path().to_path_buf(), }; @@ -1921,13 +1921,13 @@ mod tests { codex_home: codex_home.path().to_path_buf(), workspace_boundary: None, }; - let key = crate::codex_session::CodexSessionKey::new("work", workspace.path(), "gpt-5.5") + let key = crate::codex_session::CodexSessionKey::new("work", workspace.path(), "gpt-5.6") .unwrap(); crate::codex_session::CodexThreadBinding::new(key, "thread-unvalidated", None, 1) .store_at(state_root.path()) .unwrap(); let options = CodexReadinessOptions { - model: "openai-codex/gpt-5.5".to_owned(), + model: "openai-codex/gpt-5.6".to_owned(), cwd: workspace.path().to_path_buf(), state_root: state_root.path().to_path_buf(), }; @@ -2114,7 +2114,7 @@ mod tests { workspace_boundary: None, }; let options = CodexReadinessOptions { - model: "openai-codex/gpt-5.5".to_owned(), + model: "openai-codex/gpt-5.6".to_owned(), cwd: std::env::current_dir().unwrap(), state_root: state_root.path().to_path_buf(), }; @@ -2164,7 +2164,7 @@ mod tests { workspace_boundary: None, }; let options = CodexReadinessOptions { - model: "openai-codex/gpt-5.5".to_owned(), + model: "openai-codex/gpt-5.6".to_owned(), cwd: std::env::current_dir().unwrap(), state_root: state_root.path().to_path_buf(), }; diff --git a/packages/local-host-rs/src/config.rs b/packages/local-host-rs/src/config.rs index d38c937ee..350590a26 100644 --- a/packages/local-host-rs/src/config.rs +++ b/packages/local-host-rs/src/config.rs @@ -1343,7 +1343,7 @@ pub fn compose_model_route(provider: Option<&str>, model: Option<&str>) -> Optio provider.to_owned() }; let default_model = crate::model_catalog::default_model_for_provider(&model_provider) - .unwrap_or("gpt-5.5"); + .unwrap_or("gpt-5.6"); Some(format!("{provider}/{default_model}")) } } @@ -2432,6 +2432,14 @@ sandbox_mode = "danger-full-access" Some("openai/o4-mini") ); assert_eq!(compose_model_route(None, None), None); + assert_eq!( + compose_model_route(Some("openai"), None).as_deref(), + Some("openai/gpt-5.6") + ); + assert_eq!( + compose_model_route(Some("openai-codex"), None).as_deref(), + Some("openai-codex/gpt-5.6") + ); } #[test] diff --git a/packages/local-host-rs/src/credential_mode.rs b/packages/local-host-rs/src/credential_mode.rs index 359e58ca9..44c103462 100644 --- a/packages/local-host-rs/src/credential_mode.rs +++ b/packages/local-host-rs/src/credential_mode.rs @@ -1,11 +1,15 @@ -//! Product credential mode: mandatory EvalOps Identity plus managed inference -//! or local BYOK. +//! Product credential mode: managed inference through EvalOps Identity, or +//! local BYOK through a direct provider credential. //! -//! Every process must have a live EvalOps Identity session before it can start -//! a model turn. Once signed in, it can use managed inference when its session -//! has workspace scope, or a local provider credential (BYOK). There is no -//! anonymous provider path. Hosted residents instead use the established -//! tenant-bound Runner Host service credential exclusively for managed inference. +//! A managed / `evalops` / `maestro-managed` route requires a live EvalOps +//! Identity session (workspace-scoped for llm-gateway, or the hosted Runner +//! Host service credential). A direct provider explicitly selected with its +//! own API key (OpenAI, Anthropic, Google, xAI, OpenRouter, and other local +//! BYOK sources) constructs a native agent without Identity. Identity is +//! still required when no direct credential exists, and a present Identity +//! session is still verified rather than ignored. Hosted residents use the +//! established tenant-bound Runner Host service credential exclusively for +//! managed inference. use std::collections::HashMap; @@ -249,24 +253,29 @@ pub fn detect() -> Result { Ok(detect_from(snapshot.as_ref(), &env).unwrap_or(DetectedMode::Byok)) } -/// Evaluate whether a supplied Identity session can serve `model`. +/// Evaluate whether a supplied Identity session or local credential can serve +/// `model`. /// -/// An explicit `openrouter/...` route is always BYOK after Identity is +/// A direct provider with a usable local key is BYOK without Identity. An +/// explicit `openrouter/...` route stays BYOK even after Identity is /// established. Other local provider credentials also select BYOK after /// Identity, while a scoped session without a local credential uses -/// llm-gateway. Production model construction calls [`require_ready`], which -/// verifies the supplied session against Identity first; this deterministic -/// helper is retained for diagnostics and unit tests. +/// llm-gateway. Managed / `evalops` routes, Codex app-server transports, and +/// any model with no local credential still require Identity. Production +/// model construction calls [`require_ready`]; this deterministic helper is +/// retained for diagnostics and unit tests. pub fn require_ready_from( snapshot: Option<&EvalOpsCredentialSnapshot>, env: &HashMap, model: &str, ) -> Result { - let Some(session) = platform_session_from(snapshot, env) else { - bail!("{IDENTITY_REQUIRED_MESSAGE}"); - }; - - ready_mode_from_session(session, env, model) + if let Some(session) = platform_session_from(snapshot, env) { + return ready_mode_from_session(session, env, model); + } + if admits_direct_provider_without_identity(model, env) { + return Ok(DetectedMode::Byok); + } + bail!("{IDENTITY_REQUIRED_MESSAGE}"); } fn ready_mode_from_session( @@ -299,16 +308,30 @@ pub fn require_ready(model: &str) -> Result { require_ready_with_identity(model).map(|(mode, _identity)| mode) } -/// Resolve a ready provider mode together with the same live Identity session -/// that authorized it. +/// Resolve a ready provider mode together with the live Identity session that +/// authorized it, when one exists. /// -/// BYOK intentionally remains `DetectedMode::Byok`, but it is still admitted -/// only after this session is verified. Native telemetry uses the returned -/// session to bind a completed turn to its originating tenant rather than -/// rediscovering whatever account happens to be active during a later retry. -pub fn require_ready_with_identity(model: &str) -> Result<(DetectedMode, PlatformSession)> { +/// Direct-provider BYOK remains `DetectedMode::Byok` and does not contact +/// Identity when no session is present. A stored or env Identity session is +/// still verified before it authorizes managed inference or binds telemetry. +/// Native telemetry uses that session to pin a completed turn to its +/// originating tenant rather than rediscovering whatever account happens to +/// be active during a later retry. +pub fn require_ready_with_identity(model: &str) -> Result<(DetectedMode, Option)> { + let env = std::env::vars().collect::>(); + if !hosted_runner_mode(&env) + && platform_session_from(None, &env).is_none() + && crate::init_cli::load_evalops_snapshot() + .ok() + .flatten() + .is_none() + && admits_direct_provider_without_identity(model, &env) + { + return Ok((DetectedMode::Byok, None)); + } let (identity, env) = current_verified_identity_session_with_env()?; ready_mode_from_verified_identity(identity, env, model) + .map(|(mode, session)| (mode, Some(session))) } fn ready_mode_from_verified_identity( @@ -419,16 +442,20 @@ pub(crate) fn refreshed_identity_session_for_capture( const HOSTED_IDENTITY_ORIGIN: &str = "https://identity-service.evalops.svc.cluster.local:8080"; const HOSTED_IDENTITY_EXCHANGE: &str = "https://identity-service.evalops.svc.cluster.local:8080/internal/v1/kubernetes-workload-certificates/exchange"; -fn identity_verification_endpoint( - snapshot: Option<&EvalOpsCredentialSnapshot>, - env: &HashMap, -) -> Result<(String, Option)> { - let hosted = env.get("MAESTRO_HOSTED_RUNNER_MODE").is_some_and(|value| { +fn hosted_runner_mode(env: &HashMap) -> bool { + env.get("MAESTRO_HOSTED_RUNNER_MODE").is_some_and(|value| { matches!( value.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on" ) - }); + }) +} + +fn identity_verification_endpoint( + snapshot: Option<&EvalOpsCredentialSnapshot>, + env: &HashMap, +) -> Result<(String, Option)> { + let hosted = hosted_runner_mode(env); if let Some(exchange) = env.get("MAESTRO_IDENTITY_EXCHANGE_URL").filter(|_| hosted) { if exchange.trim() != HOSTED_IDENTITY_EXCHANGE { bail!("untrusted hosted EvalOps Identity exchange endpoint"); @@ -617,6 +644,20 @@ fn prefers_local_byok(model: &str) -> bool { .is_ok_and(|descriptor| descriptor.id == "openrouter") } +fn is_managed_route(model: &str) -> bool { + ProviderRegistry::resolve_descriptor(model) + .is_ok_and(|descriptor| matches!(descriptor.id, "evalops" | "maestro-managed")) +} + +/// Direct provider keys construct a native agent without Identity. +/// +/// Managed / evalops routes, Codex app-server transports, and hosted Runner +/// Host admission still require a verified Identity session. A local key +/// must not unlock those paths. +fn admits_direct_provider_without_identity(model: &str, env: &HashMap) -> bool { + !is_managed_route(model) && !is_delegated_byok_transport(model) && byok_ready(model, env) +} + pub fn platform_session_from( snapshot: Option<&EvalOpsCredentialSnapshot>, env: &HashMap, @@ -735,21 +776,21 @@ impl PlatformSession { } env.insert(ORG_ID_ENV.to_owned(), self.organization_id.clone()); env.insert(WORKSPACE_ID_ENV.to_owned(), workspace_id.to_owned()); - // Existing Identity snapshots may carry the previous OpenAI default. - // Selecting a shipped Fireworks route selects Fireworks explicitly, while - // preserving the authenticated tenant, environment, and team scope. - let select_fireworks = model + // Shipped models select a typed provider route. A provider switch uses + // that route's credential reference, retaining authenticated tenant scope. + let shipped = model .strip_prefix("evalops/") .or_else(|| model.strip_prefix("maestro-managed/")) .and_then(|model| { - crate::model_catalog::MANAGED_FIREWORKS_MODELS + crate::model_catalog::MANAGED_MODELS .iter() .find(|entry| entry.id == model) - }) - .is_some() - && provider_ref_string(&self.provider_ref, "provider").as_deref() != Some("fireworks"); - let provider = if select_fireworks { - "fireworks".to_owned() + }); + let switched = shipped.filter(|entry| { + provider_ref_string(&self.provider_ref, "provider").as_deref() != Some(entry.provider) + }); + let provider = if let Some(entry) = switched { + entry.provider.to_owned() } else { vendor_provider_id(model, &self.provider_ref)? }; @@ -761,8 +802,8 @@ impl PlatformSession { ); env.insert( CREDENTIAL_NAME_ENV.to_owned(), - if select_fireworks { - DEFAULT_MANAGED_CREDENTIAL_NAME.to_owned() + if let Some(entry) = switched { + entry.credential_name.to_owned() } else { provider_ref_string(&self.provider_ref, "credential_name") .unwrap_or_else(|| canonical_managed_credential_name(None)) @@ -1250,7 +1291,7 @@ mod tests { } #[test] - fn shipped_glm_route_uses_fireworks_with_verified_tenant_scope() { + fn shipped_managed_routes_preserve_provider_and_verified_tenant_scope() { let session = PlatformSession { access_token: "access".to_owned(), organization_id: "org-1".to_owned(), @@ -1263,14 +1304,14 @@ mod tests { "credential_name": "default" }), }; - for price in crate::model_catalog::MANAGED_FIREWORKS_MODELS { + for price in crate::model_catalog::MANAGED_MODELS { let model = format!("evalops/{}", price.id); let env = session.managed_env(&model, &HashMap::new()).unwrap(); - assert_eq!(env[PROVIDER_ENV], "fireworks"); + assert_eq!(env[PROVIDER_ENV], price.provider); assert_eq!(env[ORG_ID_ENV], "org-1"); assert_eq!(env[WORKSPACE_ID_ENV], "workspace-2"); assert_eq!(env[ENVIRONMENT_ENV], "production"); - assert_eq!(env[CREDENTIAL_NAME_ENV], "deixic-llm-gateway-glm53"); + assert_eq!(env[CREDENTIAL_NAME_ENV], price.credential_name); assert_eq!(session.managed_model_route(&model), model); assert!(!env.contains_key("FIREWORKS_API_KEY")); } @@ -1675,11 +1716,41 @@ mod tests { } #[test] - fn require_ready_rejects_byok_without_an_identity_account() { - let env = HashMap::from([("OPENROUTER_API_KEY".to_owned(), "or-test".to_owned())]); - let error = require_ready_from(None, &env, "openrouter/openai/o4-mini") - .expect_err("BYOK must not bypass EvalOps Identity"); - assert!(error.to_string().contains("deixic-code evalops login")); + fn require_ready_admits_direct_provider_keys_without_identity() { + for (model, key, value) in [ + ("openai/gpt-5.4", "OPENAI_API_KEY", "sk-test"), + ("anthropic/claude-opus-4-6", "ANTHROPIC_API_KEY", "sk-test"), + ("google/gemini-2.5-pro", "GEMINI_API_KEY", "sk-test"), + ("xai/grok-4", "XAI_API_KEY", "sk-test"), + ("openrouter/openai/o4-mini", "OPENROUTER_API_KEY", "or-test"), + ] { + let env = HashMap::from([(key.to_owned(), value.to_owned())]); + let mode = require_ready_from(None, &env, model) + .unwrap_or_else(|error| panic!("{model} with {key} must admit BYOK: {error:#}")); + assert_eq!( + mode.kind(), + CredentialModeKind::Byok, + "{model} must stay on the local provider" + ); + } + } + + #[test] + fn require_ready_still_requires_identity_for_managed_evalops_without_a_session() { + let env = HashMap::from([("OPENAI_API_KEY".to_owned(), "sk-test".to_owned())]); + for model in [ + DEFAULT_MANAGED_MODEL, + "evalops/gpt-5.5", + "maestro-managed/gpt-5.5", + ] { + let error = require_ready_from(None, &env, model).expect_err(&format!( + "{model} must not treat a local OpenAI key as managed admission" + )); + assert!( + error.to_string().contains("deixic-code evalops login"), + "{model}: {error}" + ); + } } #[test] diff --git a/packages/local-host-rs/src/doctor.rs b/packages/local-host-rs/src/doctor.rs index 647243a3f..6e7332d4f 100644 --- a/packages/local-host-rs/src/doctor.rs +++ b/packages/local-host-rs/src/doctor.rs @@ -437,7 +437,7 @@ fn credential_mode_check(readiness: &Result check( "credential_mode", CheckStatus::Pass, - "byok: EvalOps Identity and a provider route are available", + "byok: a local provider credential is available", None, false, ), @@ -1006,7 +1006,7 @@ mod tests { } #[test] - fn offline_report_requires_identity_before_reporting_byok_ready() { + fn offline_report_reports_byok_ready_without_identity() { let _lock = crate::config::test_process_env_lock(); let _restore = EnvRestore::capture(&[ "MAESTRO_HOME", @@ -1052,13 +1052,10 @@ mod tests { .iter() .find(|check| check.id == "credential_mode") .expect("credential mode check"); - assert_eq!(check.status, CheckStatus::Fail); - assert_eq!(check.summary, "EvalOps Identity is required"); - assert!( - check - .detail - .as_deref() - .is_some_and(|detail| detail.contains("deixic-code evalops login")) + assert_eq!(check.status, CheckStatus::Pass); + assert_eq!( + check.summary, + "byok: a local provider credential is available" ); } diff --git a/packages/local-host-rs/src/headless/remote_transport/tests.rs b/packages/local-host-rs/src/headless/remote_transport/tests.rs index 5e1af0365..648c5f09e 100644 --- a/packages/local-host-rs/src/headless/remote_transport/tests.rs +++ b/packages/local-host-rs/src/headless/remote_transport/tests.rs @@ -401,7 +401,7 @@ fn remote_runtime_state_snapshot_maps_into_agent_state() { controller_lease_granted: true, lease_expires_at: Some("2026-04-02T00:00:15Z".to_string()), }], - model: Some("gpt-5.4".to_string()), + model: Some("gpt-5.6".to_string()), provider: Some("openai".to_string()), session_id: Some("session-1".to_string()), cwd: Some("/tmp/project".to_string()), @@ -513,7 +513,7 @@ fn remote_runtime_state_snapshot_maps_into_agent_state() { }; let state = snapshot.into_agent_state(); - assert_eq!(state.model.as_deref(), Some("gpt-5.4")); + assert_eq!(state.model.as_deref(), Some("gpt-5.6")); assert_eq!(state.provider.as_deref(), Some("openai")); assert_eq!(state.client_protocol_version.as_deref(), Some("2026-03-30")); assert_eq!( @@ -615,7 +615,7 @@ fn remote_connection_create_request_serializes_client_tool_flags() { connection_id: Some("conn_remote".to_string()), connection_capability: Some("cap_remote".to_string()), connection_capability_required: true, - model: Some("gpt-5.4".to_string()), + model: Some("gpt-5.6".to_string()), thinking_level: Some(ThinkingLevel::Low), approval_mode: Some(ApprovalMode::Prompt), enable_client_tools: true, @@ -639,7 +639,7 @@ fn remote_connection_create_request_serializes_client_tool_flags() { assert_eq!(json["connectionId"], "conn_remote"); assert_eq!(json["connectionCapability"], "cap_remote"); assert_eq!(json["connectionCapabilityRequired"], true); - assert_eq!(json["model"], "gpt-5.4"); + assert_eq!(json["model"], "gpt-5.6"); assert_eq!(json["thinkingLevel"], "low"); assert_eq!(json["approvalMode"], "prompt"); assert_eq!(json["enableClientTools"], true); @@ -2197,7 +2197,7 @@ async fn remote_transport_connects_sends_and_receives_events() { }, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "cwd": "/tmp/project", @@ -2260,7 +2260,7 @@ async fn remote_transport_connects_sends_and_receives_events() { let cancel_token = transport.cancel_token(); assert_eq!(transport.session_id(), "sess_remote"); assert_eq!(transport.subscription_id(), "sub_remote"); - assert_eq!(transport.state().model.as_deref(), Some("gpt-5.4")); + assert_eq!(transport.state().model.as_deref(), Some("gpt-5.6")); assert_eq!(transport.state().provider.as_deref(), Some("openai")); assert_eq!(transport.state().last_status.as_deref(), Some("Attached")); assert!( @@ -2443,7 +2443,7 @@ async fn remote_viewer_transport_rejects_controller_messages() { "cursor": 1, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], @@ -2524,7 +2524,7 @@ async fn remote_transport_updates_cached_state_on_snapshot_events() { }, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], @@ -2546,7 +2546,7 @@ async fn remote_transport_updates_cached_state_on_snapshot_events() { }, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], @@ -2615,7 +2615,7 @@ async fn remote_transport_ignores_replayed_events_that_do_not_advance_cursor() { "cursor": 1, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], @@ -2703,7 +2703,7 @@ async fn remote_transport_accepts_heartbeat_without_cursor_advance() { "cursor": 1, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], @@ -2960,7 +2960,7 @@ async fn remote_transport_ignores_malformed_events_and_keeps_streaming() { "cursor": 1, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], @@ -3099,7 +3099,7 @@ async fn remote_transport_surfaces_stream_closure_without_internal_reader_retry( "cursor": 1, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], @@ -3579,7 +3579,7 @@ async fn remote_transport_applies_reset_events_as_snapshots() { }, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], @@ -3602,7 +3602,7 @@ async fn remote_transport_applies_reset_events_as_snapshots() { }, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], diff --git a/packages/local-host-rs/src/headless/supervisor/tests.rs b/packages/local-host-rs/src/headless/supervisor/tests.rs index 98a4e4fc7..d28990884 100644 --- a/packages/local-host-rs/src/headless/supervisor/tests.rs +++ b/packages/local-host-rs/src/headless/supervisor/tests.rs @@ -1062,7 +1062,7 @@ fn resume_recorded_session_restores_replay_and_recorder() { recorder .record_received(&FromAgentMessage::Ready { protocol_version: Some("2026-03-30".to_string()), - model: "gpt-5.4".to_string(), + model: "gpt-5.6".to_string(), provider: "openai".to_string(), session_id: Some("sess_saved".to_string()), }) @@ -1075,7 +1075,7 @@ fn resume_recorded_session_restores_replay_and_recorder() { .expect("resume builder") .build(); - assert_eq!(supervisor.state().model.as_deref(), Some("gpt-5.4")); + assert_eq!(supervisor.state().model.as_deref(), Some("gpt-5.6")); assert_eq!(supervisor.state().provider.as_deref(), Some("openai")); assert_eq!(supervisor.state().session_id.as_deref(), Some("sess_saved")); assert!(supervisor.session_recorder.is_some()); @@ -1273,7 +1273,7 @@ fn apply_snapshot_keeps_session_recorder_state_in_sync() { let mut supervisor = AgentSupervisor::new(SupervisorConfig::default()).with_session_recorder(recorder); let snapshot_state = AgentState { - model: Some("gpt-5.4".to_string()), + model: Some("gpt-5.6".to_string()), provider: Some("openai".to_string()), session_id: Some("sess_remote".to_string()), is_ready: true, @@ -2110,7 +2110,7 @@ async fn remote_connect_hydrates_state_without_replaying_init() { }, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "cwd": "/tmp/project", @@ -2142,7 +2142,7 @@ async fn remote_connect_hydrates_state_without_replaying_init() { supervisor.connect().await.expect("connect"); - assert_eq!(supervisor.state().model.as_deref(), Some("gpt-5.4")); + assert_eq!(supervisor.state().model.as_deref(), Some("gpt-5.6")); assert_eq!(supervisor.state().provider.as_deref(), Some("openai")); assert_eq!( supervisor.state().session_id.as_deref(), @@ -2203,7 +2203,7 @@ async fn recv_prioritizes_supervisor_events_before_remote_agent_messages() { }, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "cwd": "/tmp/project", @@ -2393,7 +2393,7 @@ async fn non_retryable_remote_disconnect_shuts_down_transport() { "cursor": 0, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], @@ -2617,7 +2617,7 @@ async fn successful_remote_connect_resets_stale_reference_retry_budget() { "cursor": 0, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], @@ -2651,7 +2651,7 @@ async fn remote_auto_reconnect_reuses_bootstrapped_session_id() { }, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "cwd": "/tmp/project", @@ -2887,7 +2887,7 @@ async fn remote_auto_reconnect_reuses_previous_connection_id_without_take_contro "cursor": 0, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], @@ -3178,7 +3178,7 @@ async fn clean_remote_disconnect_does_not_force_take_control_on_manual_reconnect "cursor": 0, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], @@ -3242,7 +3242,7 @@ async fn explicit_disconnect_after_failed_reconnect_clears_private_resume_author "cursor": 0, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], @@ -3331,7 +3331,7 @@ async fn manual_remote_reconnect_shuts_down_existing_transport() { "cursor": 0, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], @@ -3378,7 +3378,7 @@ async fn manual_remote_reconnect_disconnects_before_next_bootstrap() { "cursor": 0, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], @@ -3432,7 +3432,7 @@ async fn auto_remote_reconnect_waits_for_disconnect_completion_before_next_boots "cursor": 0, "state": { "protocol_version": "2026-03-30", - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "sess_remote", "pending_approvals": [], diff --git a/packages/local-host-rs/src/headless_server.rs b/packages/local-host-rs/src/headless_server.rs index 98ccfafd6..ab941b48f 100644 --- a/packages/local-host-rs/src/headless_server.rs +++ b/packages/local-host-rs/src/headless_server.rs @@ -5088,7 +5088,7 @@ mod tests { ) .env_remove("MAESTRO_MODEL") .env_remove("OPENAI_API_KEY") - .env("MAESTRO_DEFAULT_MODEL", "evalops/gpt-5.5") + .env("MAESTRO_DEFAULT_MODEL", "evalops/gpt-5.6") // Parallel tests can install a direct token in the parent process. // This fixture must exercise its explicit token-file credential. .env_remove(crate::credential_mode::ACCESS_TOKEN_ENV) @@ -5123,11 +5123,160 @@ mod tests { .find_map(|line| serde_json::from_str::(line).ok()) .expect("first protocol message"); assert_eq!(ready["type"], "ready"); - assert_eq!(ready["model"], "evalops/gpt-5.5"); + assert_eq!(ready["model"], "evalops/gpt-5.6"); assert_eq!(ready["provider"], "openrouter"); assert!(!String::from_utf8_lossy(&output.stderr).contains("OPENAI_API_KEY")); } + fn spawn_headless_identity_fixture( + test_name: &str, + fixture_env: &str, + extra_env: &[(&str, &std::ffi::OsStr)], + ) -> std::process::Output { + let home = tempfile::tempdir().expect("fixture Maestro home"); + let current = std::env::current_exe().expect("current test binary"); + let mut command = std::process::Command::new(current); + command + .arg(test_name) + .arg("--exact") + .arg("--nocapture") + .arg("--format") + .arg("terse") + .env(fixture_env, "1") + .env("MAESTRO_HOME", home.path()) + .env("MAESTRO_OAUTH_STORAGE_MODE", "file") + .env("MAESTRO_DISABLE_KEYCHAIN", "1") + .env_remove("MAESTRO_MODEL") + .env_remove("MAESTRO_DEFAULT_MODEL") + .env_remove("MAESTRO_IDENTITY_URL") + .env_remove(crate::credential_mode::ACCESS_TOKEN_ENV) + .env_remove(crate::credential_mode::ACCESS_TOKEN_FILE_ENV) + .env_remove(crate::credential_mode::ORG_ID_ENV) + .env_remove(crate::credential_mode::WORKSPACE_ID_ENV) + .env_remove("MAESTRO_HOSTED_RUNNER_MODE") + .env_remove(crate::init_cli::TEST_IDENTITY_AUTHORITY_ENV); + for (key, value) in extra_env { + command.env(key, value); + } + command + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .output() + .expect("run headless identity fixture") + } + + #[tokio::test] + async fn direct_openai_key_reaches_headless_ready_without_identity() { + if std::env::var_os("MAESTRO_HEADLESS_DIRECT_OPENAI_READY_FIXTURE").is_some() { + assert_eq!( + run_headless_server(Some("openai/gpt-5.4".to_string())) + .await + .expect("direct OpenAI headless fixture"), + 0 + ); + return; + } + + let home = tempfile::tempdir().expect("fixture Maestro home"); + let current = std::env::current_exe().expect("current test binary"); + let mut child = std::process::Command::new(current) + .arg( + "headless_server::tests::direct_openai_key_reaches_headless_ready_without_identity", + ) + .arg("--exact") + .arg("--nocapture") + .arg("--format") + .arg("terse") + .env("MAESTRO_HEADLESS_DIRECT_OPENAI_READY_FIXTURE", "1") + .env("MAESTRO_HOME", home.path()) + .env("MAESTRO_OAUTH_STORAGE_MODE", "file") + .env("MAESTRO_DISABLE_KEYCHAIN", "1") + .env("OPENAI_API_KEY", "synthetic-maestro-smoke-key") + .env("OPENAI_BASE_URL", "http://127.0.0.1:9/v1") + .env_remove("MAESTRO_MODEL") + .env_remove("MAESTRO_DEFAULT_MODEL") + .env_remove("MAESTRO_IDENTITY_URL") + .env_remove(crate::credential_mode::ACCESS_TOKEN_ENV) + .env_remove(crate::credential_mode::ACCESS_TOKEN_FILE_ENV) + .env_remove(crate::credential_mode::ORG_ID_ENV) + .env_remove(crate::credential_mode::WORKSPACE_ID_ENV) + .env_remove("MAESTRO_HOSTED_RUNNER_MODE") + .env_remove(crate::init_cli::TEST_IDENTITY_AUTHORITY_ENV) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .expect("spawn direct OpenAI headless fixture"); + writeln!( + child.stdin.take().expect("fixture stdin"), + "{}", + json!({"type":"shutdown"}) + ) + .expect("write shutdown"); + let output = child.wait_with_output().expect("headless fixture output"); + assert!( + output.status.success(), + "fixture failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let ready = String::from_utf8_lossy(&output.stdout) + .lines() + .find_map(|line| serde_json::from_str::(line).ok()) + .expect("first protocol message"); + assert_eq!(ready["type"], "ready"); + assert_eq!(ready["model"], "openai/gpt-5.4"); + assert_eq!(ready["provider"], "OpenAI"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.contains(crate::credential_mode::IDENTITY_REQUIRED_MESSAGE), + "{stderr}" + ); + } + + #[tokio::test] + async fn managed_evalops_headless_route_requires_identity() { + if std::env::var_os("MAESTRO_HEADLESS_MANAGED_IDENTITY_REQUIRED_FIXTURE").is_some() { + let error = run_headless_server(Some("evalops/gpt-5.5".to_string())) + .await + .expect_err("managed evalops headless must not become ready without Identity"); + let rendered = format!("{error:#}"); + assert!( + rendered.contains(crate::credential_mode::IDENTITY_REQUIRED_MESSAGE), + "{rendered}" + ); + eprintln!("{rendered}"); + return; + } + + let output = spawn_headless_identity_fixture( + "headless_server::tests::managed_evalops_headless_route_requires_identity", + "MAESTRO_HEADLESS_MANAGED_IDENTITY_REQUIRED_FIXTURE", + &[ + ( + "OPENAI_API_KEY", + std::ffi::OsStr::new("synthetic-maestro-smoke-key"), + ), + ( + "OPENAI_BASE_URL", + std::ffi::OsStr::new("http://127.0.0.1:9/v1"), + ), + ], + ); + assert!( + output.status.success(), + "fixture failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + String::from_utf8_lossy(&output.stderr) + .contains(crate::credential_mode::IDENTITY_REQUIRED_MESSAGE), + "stdout: {}; stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + struct HeadlessFixtureChild { child: std::process::Child, #[cfg(unix)] diff --git a/packages/local-host-rs/src/hosted_runner.rs b/packages/local-host-rs/src/hosted_runner.rs index 2f8738f58..7fbad87e7 100644 --- a/packages/local-host-rs/src/hosted_runner.rs +++ b/packages/local-host-rs/src/hosted_runner.rs @@ -5844,7 +5844,10 @@ where body.len() ); socket.write_all(headers.as_bytes()).await?; - socket.write_all(body).await + socket.write_all(body).await?; + // TLS may accept plaintext while ciphertext is still buffered. Drain it + // before handle_socket drops this single-response connection. + socket.flush().await } async fn write_sse_headers(socket: &mut S) -> io::Result<()> @@ -5855,7 +5858,8 @@ where .write_all( b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\nConnection: keep-alive\r\n\r\n", ) - .await + .await?; + socket.flush().await } async fn write_sse_event(socket: &mut S, envelope: &StreamEnvelope) -> io::Result<()> @@ -5866,7 +5870,8 @@ where .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; socket.write_all(b"data: ").await?; socket.write_all(payload.as_bytes()).await?; - socket.write_all(b"\n\n").await + socket.write_all(b"\n\n").await?; + socket.flush().await } fn status_reason(status: u16) -> &'static str { diff --git a/packages/local-host-rs/src/hosted_runner/tests.rs b/packages/local-host-rs/src/hosted_runner/tests.rs index 713762e8c..e21de09cc 100644 --- a/packages/local-host-rs/src/hosted_runner/tests.rs +++ b/packages/local-host-rs/src/hosted_runner/tests.rs @@ -15,6 +15,8 @@ use crate::headless::messages::{CodexSubagentContinuityEdge, ToolRetryDecisionAc use crate::headless::{NativeToolCapability, PendingApproval}; use crate::hosted_runner::rendezvous_protocol::RendezvousMode; +mod transport_flush; + #[test] fn append_turn_dispatch_preserves_managed_inference_authorization() { let message = append_turn_message(AppendTurnRequest { @@ -6437,7 +6439,7 @@ fn snapshot_manifest_parser_accepts_typescript_hosted_shape() { "connection_count": 0, "subscriber_count": 0, "connections": [], - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "openai", "session_id": "session_ts", "cwd": workspace.path(), @@ -6554,7 +6556,7 @@ fn runtime_state_snapshot_serializes_empty_codex_subagent_edges() { controller_subscription_id: None, controller_connection_id: None, connections: Vec::new(), - model: Some("gpt-5.4".to_string()), + model: Some("gpt-5.6".to_string()), provider: Some("rust".to_string()), session_id: Some("session_empty_edges".to_string()), cwd: None, @@ -6603,7 +6605,7 @@ fn work_continuity_manifest_extracts_codex_subagent_ids() { "connection_count": 0, "subscriber_count": 0, "connections": [], - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "rust", "session_id": "session_rust", "pending_approvals": [{ @@ -6699,7 +6701,7 @@ fn work_continuity_manifest_preserves_restored_codex_subagent_edges() { "connection_count": 0, "subscriber_count": 0, "connections": [], - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "rust", "session_id": "session_rust_restored", "pending_approvals": [], @@ -6792,7 +6794,7 @@ fn work_continuity_manifest_counts_mixed_codex_and_regular_tools() { "connection_count": 0, "subscriber_count": 0, "connections": [], - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "rust", "session_id": "session_rust_mixed", "pending_approvals": [], @@ -6861,7 +6863,7 @@ fn work_continuity_manifest_keeps_spawned_and_resumed_codex_subagents_active() { "connection_count": 0, "subscriber_count": 0, "connections": [], - "model": "gpt-5.4", + "model": "gpt-5.6", "provider": "rust", "session_id": "session_rust_active_subagents", "pending_approvals": [], @@ -9096,7 +9098,7 @@ async fn state_snapshot_redacts_sensitive_supervisor_state() { }, ); let supervisor_state = AgentState { - model: Some("gpt-5.4".to_string()), + model: Some("gpt-5.6".to_string()), provider: Some("openai".to_string()), session_id: Some("supervisor-session-1".to_string()), cwd: Some("/runtime/workspace".to_string()), @@ -9153,7 +9155,7 @@ async fn state_snapshot_redacts_sensitive_supervisor_state() { .json() .await .expect("controller json"); - assert_eq!(controller["snapshot"]["state"]["model"], "gpt-5.4"); + assert_eq!(controller["snapshot"]["state"]["model"], "gpt-5.6"); assert_eq!(controller["snapshot"]["state"]["connection_count"], 1); let subscribe: serde_json::Value = client @@ -9191,7 +9193,7 @@ async fn state_snapshot_redacts_sensitive_supervisor_state() { .json() .await .expect("state json"); - assert_eq!(state["state"]["model"], "gpt-5.4"); + assert_eq!(state["state"]["model"], "gpt-5.6"); assert_eq!(state["state"]["provider"], "openai"); assert_eq!(state["state"]["session_id"], "supervisor-session-1"); assert_eq!(state["state"]["cwd"], "/runtime/workspace"); diff --git a/packages/local-host-rs/src/hosted_runner/tests/transport_flush.rs b/packages/local-host-rs/src/hosted_runner/tests/transport_flush.rs new file mode 100644 index 000000000..1a65b6337 --- /dev/null +++ b/packages/local-host-rs/src/hosted_runner/tests/transport_flush.rs @@ -0,0 +1,90 @@ +use super::*; +use tokio::io::{AsyncReadExt, BufWriter}; + +#[tokio::test] +async fn http_frames_are_visible_before_the_writer_returns() { + // Like tokio-rustls, BufWriter may accept a complete frame without + // delivering it to the underlying stream until flush is polled. + let mut socket = BufWriter::with_capacity(4096, Vec::new()); + write_response(&mut socket, 200, "application/json", b"{\"ok\":true}") + .await + .unwrap(); + assert!(socket.get_ref().ends_with(b"\r\n\r\n{\"ok\":true}")); + + let mut socket = BufWriter::with_capacity(4096, Vec::new()); + write_sse_headers(&mut socket).await.unwrap(); + assert!(socket.get_ref().ends_with(b"\r\n\r\n")); + let header_len = socket.get_ref().len(); + let envelope = stream_message( + 1, + FromAgentMessage::ResponseStart { + response_id: "flush-test".into(), + }, + ); + write_sse_event(&mut socket, &envelope).await.unwrap(); + let frame = &socket.get_ref()[header_len..]; + assert!(frame.starts_with(b"data: ")); + assert!(frame.ends_with(b"\n\n")); + assert_eq!( + &frame[6..frame.len() - 2], + serde_json::to_vec(&envelope).unwrap() + ); +} + +#[tokio::test] +async fn tls_response_survives_backpressure_before_connection_drop() { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let provider = Arc::new(rustls::crypto::ring::default_provider()); + let mut server_config = rustls::ServerConfig::builder_with_provider(provider.clone()) + .with_safe_default_protocol_versions() + .unwrap() + .with_no_client_auth() + .with_single_cert( + vec![cert.cert.der().clone()], + rustls::pki_types::PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()).into(), + ) + .unwrap(); + server_config.send_tls13_tickets = 0; + let mut roots = rustls::RootCertStore::empty(); + roots.add(cert.cert.der().clone()).unwrap(); + let client_config = rustls::ClientConfig::builder_with_provider(provider) + .with_safe_default_protocol_versions() + .unwrap() + .with_root_certificates(roots) + .with_no_client_auth(); + let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(server_config)); + let connector = tokio_rustls::TlsConnector::from(Arc::new(client_config)); + + // A TLS record exceeds this transport capacity. The writer must wait for + // the reader rather than dropping accepted but still-buffered ciphertext. + let (server_io, client_io) = tokio::io::duplex(64); + let exchange = async { + let (server, client) = tokio::join!( + acceptor.accept(server_io), + connector.connect("localhost".try_into().unwrap(), client_io) + ); + let mut server = server.unwrap(); + let mut client = client.unwrap(); + let body = vec![b'x'; 4096]; + let expected = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + String::from_utf8(body.clone()).unwrap() + ); + let send = async { + write_response(&mut server, 200, "application/json", &body) + .await + .unwrap(); + drop(server); + }; + let receive = async { + let mut received = vec![0; expected.len()]; + client.read_exact(&mut received).await.unwrap(); + assert_eq!(received, expected.as_bytes()); + }; + tokio::join!(send, receive); + }; + tokio::time::timeout(Duration::from_secs(5), exchange) + .await + .expect("TLS response must drain without another application write"); +} diff --git a/packages/local-host-rs/src/model_catalog.rs b/packages/local-host-rs/src/model_catalog.rs index 6b383b890..46dd3b35a 100644 --- a/packages/local-host-rs/src/model_catalog.rs +++ b/packages/local-host-rs/src/model_catalog.rs @@ -186,8 +186,8 @@ fn now_epoch_secs() -> u64 { pub fn default_model_for_provider(provider: &str) -> Option<&'static str> { match provider.trim().to_ascii_lowercase().as_str() { "anthropic" | "claude" => Some("claude-sonnet-4-6"), - "openai" => Some("gpt-5.5"), - "openai-codex" | "codex" => Some("gpt-5.5"), + "openai" => Some("gpt-5.6"), + "openai-codex" | "codex" => Some("gpt-5.6"), "google" | "gemini" | "vertex-ai" | "vertex" => Some("gemini-2.5-pro"), "xai" | "grok" => Some("grok-4.5"), "openrouter" => Some("openai/gpt-4o-mini"), @@ -223,7 +223,7 @@ pub fn available_models() -> Vec { } } } - append_managed_fireworks_models(&mut models); + append_managed_models(&mut models); append_builtin_local_models(&mut models); mirror_vertex_models(&mut models); models @@ -240,40 +240,76 @@ pub fn model_route(model: &ModelInfo) -> String { } } -pub struct ManagedFireworksModel { +pub struct ManagedModel { pub id: &'static str, pub name: &'static str, + pub provider: &'static str, + pub credential_name: &'static str, + pub context_tokens: u32, } /// Shipped managed route metadata. Gateway admission and pricing remain server-owned. -pub const MANAGED_FIREWORKS_MODELS: &[ManagedFireworksModel] = &[ - ManagedFireworksModel { +pub const MANAGED_MODELS: &[ManagedModel] = &[ + ManagedModel { id: "accounts/fireworks/models/glm-5p3", name: "GLM-5.3", + provider: "fireworks", + credential_name: crate::credential_mode::DEFAULT_MANAGED_CREDENTIAL_NAME, + context_tokens: 1_048_576, }, - ManagedFireworksModel { + ManagedModel { id: "accounts/fireworks/models/glm-5p3-flash", name: "GLM-5.3 Flash", + provider: "fireworks", + credential_name: crate::credential_mode::DEFAULT_MANAGED_CREDENTIAL_NAME, + context_tokens: 1_048_576, }, - ManagedFireworksModel { + ManagedModel { id: "accounts/fireworks/models/deepseek-v4-flash-0731", name: "DeepSeek V4 Flash 0731", + provider: "fireworks", + credential_name: crate::credential_mode::DEFAULT_MANAGED_CREDENTIAL_NAME, + context_tokens: 1_048_576, }, - ManagedFireworksModel { + ManagedModel { id: "accounts/fireworks/models/kimi-k3", name: "Kimi K3", + provider: "fireworks", + credential_name: crate::credential_mode::DEFAULT_MANAGED_CREDENTIAL_NAME, + context_tokens: 1_048_576, + }, + ManagedModel { + id: "accounts/fireworks/models/deepseek-v4p1-flash", + name: "DeepSeek V4.1 Flash", + provider: "fireworks", + credential_name: "deixic-llm-gateway-glm53", + context_tokens: 1_048_576, + }, + ManagedModel { + id: "accounts/fireworks/models/qwen3p8-max", + name: "Qwen 3.8 Max", + provider: "fireworks", + credential_name: "deixic-llm-gateway-glm53", + context_tokens: 131_072, + }, + ManagedModel { + id: "gemini-3.8-flash", + name: "Gemini 3.8 Flash", + provider: "google", + credential_name: "deixic-llm-gateway-gemini38", + context_tokens: 1_048_576, }, ]; // Managed routes are product-owned typed contracts, independent of community cache refreshes. -fn append_managed_fireworks_models(models: &mut Vec) { - for price in MANAGED_FIREWORKS_MODELS { +fn append_managed_models(models: &mut Vec) { + for price in MANAGED_MODELS { models.retain(|model| !(model.provider == "evalops" && model.id == price.id)); models.push(ModelInfo { id: price.id.to_owned(), name: price.name.to_owned(), provider: "evalops".to_owned(), - description: "Fireworks · Model credits · Text and tools".to_owned(), + description: format!("{} · Model credits · Text and tools", price.provider), capabilities: ModelCapabilities { protocol: ModelProtocol::OpenAiChat, tools: true, @@ -282,13 +318,13 @@ fn append_managed_fireworks_models(models: &mut Vec) { // Reasoning controls are not part of the prepaid request contract. reasoning: false, streaming: true, - context_tokens: 1_048_576, + context_tokens: price.context_tokens, // Keep the existing 16k default; the gateway enforces its 32k ceiling. output_tokens: None, }, verification: ModelVerification { state: VerificationState::Catalog, - source: "managed-fireworks-contract".to_owned(), + source: "managed-model-contract".to_owned(), detail: Some( "Availability requires a configured managed route and model credits." .to_owned(), @@ -1208,7 +1244,7 @@ pub fn has_provider_mismatch(id: &str) -> bool { return false; } let models = available_models(); - // Prefer an exact provider+id hit (openai-codex/gpt-5.5 vs openai/gpt-5.5). + // Prefer an exact provider+id hit (openai-codex/gpt-5.6 vs openai/gpt-5.6). if models .iter() .any(|model| model.id == bare_id && model.provider == descriptor.id) @@ -1302,12 +1338,12 @@ mod tests { } #[test] - fn managed_fireworks_routes_survive_cache_and_preserve_provider_and_limits() { + fn managed_model_routes_survive_cache_and_preserve_provider_and_limits() { let mut models = Vec::new(); - append_managed_fireworks_models(&mut models); - append_managed_fireworks_models(&mut models); - assert_eq!(models.len(), 4); - for price in MANAGED_FIREWORKS_MODELS { + append_managed_models(&mut models); + append_managed_models(&mut models); + assert_eq!(models.len(), 7); + for price in MANAGED_MODELS { let route = format!("evalops/{}", price.id); let model = find_model(&route).unwrap(); assert_eq!(model_route(&model), route); @@ -1489,6 +1525,9 @@ mod tests { .unwrap_or_else(|| panic!("{provider}/{default} must be in the bundled catalog")); assert_eq!(model.provider, provider); } + assert_eq!(default_model_for_provider("openai"), Some("gpt-5.6")); + assert_eq!(default_model_for_provider("openai-codex"), Some("gpt-5.6")); + assert_eq!(default_model_for_provider("codex"), Some("gpt-5.6")); assert_eq!(default_model_for_provider("nope"), None); assert_eq!( default_model_for_provider("vertex-ai"), @@ -1818,7 +1857,7 @@ mod tests { #[test] fn catalog_exposes_capabilities_separately_from_verification() { - let model = find_model("openai/gpt-5.5").expect("catalog model"); + let model = find_model("openai/gpt-5.6").expect("catalog model"); assert!(model.capabilities.tools); assert!(model.capabilities.reasoning); assert_eq!(model.capabilities.protocol, ModelProtocol::OpenAiResponses); @@ -1836,8 +1875,8 @@ mod tests { assert!(find_model("claude/claude-sonnet-4-6").is_some()); assert!(!has_provider_mismatch("openai/custom-model")); // openai-codex is subscription transport over OpenAI catalog ids. - assert!(find_model("openai-codex/gpt-5.5").is_some()); - assert!(!has_provider_mismatch("openai-codex/gpt-5.5")); + assert!(find_model("openai-codex/gpt-5.6").is_some()); + assert!(!has_provider_mismatch("openai-codex/gpt-5.6")); } #[test] @@ -1877,8 +1916,8 @@ mod tests { "top_provider": {"max_completion_tokens": 64_000} }, { - "id": "openai/gpt-5.4", - "name": "GPT-5.4", + "id": "openai/gpt-5.6", + "name": "GPT-5.6", "context_length": 400_000, "supported_parameters": ["tools"] }, @@ -1902,7 +1941,7 @@ mod tests { assert!(models[0].capabilities.vision); assert!(models[0].capabilities.reasoning); assert_eq!(models[0].capabilities.output_tokens, Some(64_000)); - assert_eq!(models[1].id, "openai/gpt-5.4"); + assert_eq!(models[1].id, "openai/gpt-5.6"); assert_eq!(models[1].capabilities.protocol, ModelProtocol::OpenAiChat); assert_eq!(models[1].capabilities.output_tokens, None); assert!(!models.iter().any(|model| model.id.ends_with(":batch"))); diff --git a/packages/local-host-rs/src/model_catalog_data.json b/packages/local-host-rs/src/model_catalog_data.json index ce4139882..7fa0b8c63 100644 --- a/packages/local-host-rs/src/model_catalog_data.json +++ b/packages/local-host-rs/src/model_catalog_data.json @@ -1,5 +1,5 @@ { - "generated_at": 1789198032, + "generated_at": 1789284488, "source": "https://models.dev/api.json+https://openrouter.ai/api/v1/models", "models": [ { @@ -1238,9 +1238,9 @@ }, { "id": "~anthropic/claude-haiku-latest", - "name": "Anthropic Claude Haiku Latest", + "name": "Anthropic: Claude Haiku Latest", "provider": "openrouter", - "description": "This model always redirects to the latest model in the Anthropic Claude Haiku family.", + "description": "This model always redirects to the latest model in the Claude Haiku family.", "capabilities": { "protocol": "openai-chat", "tools": true, @@ -1276,9 +1276,9 @@ }, { "id": "~anthropic/claude-sonnet-latest", - "name": "Anthropic Claude Sonnet Latest", + "name": "Anthropic: Claude Sonnet Latest", "provider": "openrouter", - "description": "This model always redirects to the latest model in the Anthropic Claude Sonnet family.", + "description": "This model always redirects to the latest model in the Claude Sonnet family.", "capabilities": { "protocol": "openai-chat", "tools": true, @@ -1295,7 +1295,7 @@ }, { "id": "~deepseek/deepseek-v4-flash-latest", - "name": "DeepSeek V4 Flash Latest", + "name": "DeepSeek: DeepSeek V4 Flash Latest", "provider": "openrouter", "description": "This model always redirects to the latest model in the DeepSeek V4 Flash family.", "capabilities": { @@ -1305,7 +1305,7 @@ "reasoning": true, "streaming": true, "context_tokens": 1310720, - "output_tokens": 943718 + "output_tokens": 131072 }, "verification": { "state": "catalog", @@ -1314,9 +1314,9 @@ }, { "id": "~google/gemini-flash-latest", - "name": "Google Gemini Flash Latest", + "name": "Google: Gemini Flash Latest", "provider": "openrouter", - "description": "This model always redirects to the latest model in the Google Gemini Flash family.", + "description": "This model always redirects to the latest model in the Gemini Flash family.", "capabilities": { "protocol": "openai-chat", "tools": true, @@ -1333,9 +1333,9 @@ }, { "id": "~google/gemini-pro-latest", - "name": "Google Gemini Pro Latest", + "name": "Google: Gemini Pro Latest", "provider": "openrouter", - "description": "This model always redirects to the latest model in the Google Gemini Pro family.", + "description": "This model always redirects to the latest model in the Gemini Pro family.", "capabilities": { "protocol": "openai-chat", "tools": true, @@ -1352,9 +1352,9 @@ }, { "id": "~moonshotai/kimi-latest", - "name": "MoonshotAI Kimi Latest", + "name": "MoonshotAI: Kimi Latest", "provider": "openrouter", - "description": "This model always redirects to the latest model in the MoonshotAI Kimi family.", + "description": "This model always redirects to the latest model in the Kimi family.", "capabilities": { "protocol": "openai-chat", "tools": true, @@ -1371,9 +1371,9 @@ }, { "id": "~openai/gpt-astra-latest", - "name": "OpenAI GPT Astra Latest", + "name": "OpenAI: GPT Astra Latest", "provider": "openrouter", - "description": "This model always redirects to the latest model in the OpenAI GPT Astra family.", + "description": "This model always redirects to the latest model in the GPT Astra family.", "capabilities": { "protocol": "openai-chat", "tools": true, @@ -1390,9 +1390,9 @@ }, { "id": "~openai/gpt-luna-latest", - "name": "OpenAI GPT Luna Latest", + "name": "OpenAI: GPT Luna Latest", "provider": "openrouter", - "description": "This model always redirects to the latest model in the OpenAI GPT Luna family.", + "description": "This model always redirects to the latest model in the GPT Luna family.", "capabilities": { "protocol": "openai-chat", "tools": true, @@ -1409,9 +1409,9 @@ }, { "id": "~openai/gpt-mini-latest", - "name": "OpenAI GPT Mini Latest", + "name": "OpenAI: GPT Mini Latest", "provider": "openrouter", - "description": "This model always redirects to the latest model in the OpenAI GPT Mini family.", + "description": "This model always redirects to the latest model in the GPT Mini family.", "capabilities": { "protocol": "openai-chat", "tools": true, @@ -1428,9 +1428,9 @@ }, { "id": "~openai/gpt-sol-latest", - "name": "OpenAI GPT Sol Latest", + "name": "OpenAI: GPT Sol Latest", "provider": "openrouter", - "description": "This model always redirects to the latest model in the OpenAI GPT Sol family.", + "description": "This model always redirects to the latest model in the GPT Sol family.", "capabilities": { "protocol": "openai-chat", "tools": true, @@ -1447,9 +1447,9 @@ }, { "id": "~openai/gpt-terra-latest", - "name": "OpenAI GPT Terra Latest", + "name": "OpenAI: GPT Terra Latest", "provider": "openrouter", - "description": "This model always redirects to the latest model in the OpenAI GPT Terra family.", + "description": "This model always redirects to the latest model in the GPT Terra family.", "capabilities": { "protocol": "openai-chat", "tools": true, @@ -1514,7 +1514,7 @@ "reasoning": true, "streaming": true, "context_tokens": 1310720, - "output_tokens": 943718 + "output_tokens": 235929 }, "verification": { "state": "catalog", @@ -2521,7 +2521,7 @@ "reasoning": true, "streaming": true, "context_tokens": 1048576, - "output_tokens": 384000 + "output_tokens": 393216 }, "verification": { "state": "catalog", @@ -3072,7 +3072,7 @@ "reasoning": true, "streaming": true, "context_tokens": 262144, - "output_tokens": 32768 + "output_tokens": 235929 }, "verification": { "state": "catalog", @@ -3528,7 +3528,7 @@ "reasoning": false, "streaming": true, "context_tokens": 131072, - "output_tokens": 8192 + "output_tokens": 16384 }, "verification": { "state": "catalog", @@ -3927,7 +3927,7 @@ "reasoning": true, "streaming": true, "context_tokens": 204800, - "output_tokens": 131072 + "output_tokens": 128000 }, "verification": { "state": "catalog", @@ -4345,7 +4345,7 @@ "reasoning": false, "streaming": true, "context_tokens": 131072, - "output_tokens": 100352 + "output_tokens": 98304 }, "verification": { "state": "catalog", @@ -4364,7 +4364,7 @@ "reasoning": false, "streaming": true, "context_tokens": 262144, - "output_tokens": 100352 + "output_tokens": 98304 }, "verification": { "state": "catalog", @@ -4383,7 +4383,7 @@ "reasoning": true, "streaming": true, "context_tokens": 262144, - "output_tokens": 100352 + "output_tokens": 98304 }, "verification": { "state": "catalog", @@ -6451,7 +6451,7 @@ "reasoning": false, "streaming": true, "context_tokens": 262144, - "output_tokens": 235929 + "output_tokens": 32000 }, "verification": { "state": "catalog", @@ -6600,7 +6600,7 @@ "protocol": "openai-chat", "tools": true, "vision": false, - "reasoning": true, + "reasoning": false, "streaming": true, "context_tokens": 1000000, "output_tokens": 65536 @@ -7648,7 +7648,7 @@ "reasoning": true, "streaming": true, "context_tokens": 1048576, - "output_tokens": 32768 + "output_tokens": 471859 }, "verification": { "state": "catalog", @@ -8161,7 +8161,7 @@ "reasoning": true, "streaming": true, "context_tokens": 1310720, - "output_tokens": 943718 + "output_tokens": 131072 }, "verification": { "state": "catalog", diff --git a/packages/maestro-rs/Cargo.toml b/packages/maestro-rs/Cargo.toml index 3109e1743..9a61e1fee 100644 --- a/packages/maestro-rs/Cargo.toml +++ b/packages/maestro-rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "maestro" -version = "0.10.86" +version = "0.10.88" edition = "2021" license = "MIT" description = "Canonical native Rust CLI for Deixic Code" diff --git a/packages/runtime-gateway-rs/src/a2a/mod.rs b/packages/runtime-gateway-rs/src/a2a/mod.rs index 6f48996ec..80863f3b9 100644 --- a/packages/runtime-gateway-rs/src/a2a/mod.rs +++ b/packages/runtime-gateway-rs/src/a2a/mod.rs @@ -49,5 +49,6 @@ pub(crate) use tasks::{ }; #[cfg(test)] pub(crate) use tasks::{ - a2a_acceptance_report_artifacts, a2a_task_visible_to_auth, cancel_a2a_task, + a2a_acceptance_report_artifacts, a2a_task_visible_to_auth, a2a_vfs_hydration_gap, + cancel_a2a_task, completion_subagent_capsule_from_metadata, }; diff --git a/packages/runtime-gateway-rs/src/a2a/tasks.rs b/packages/runtime-gateway-rs/src/a2a/tasks.rs index 8f43001c9..612489fdc 100644 --- a/packages/runtime-gateway-rs/src/a2a/tasks.rs +++ b/packages/runtime-gateway-rs/src/a2a/tasks.rs @@ -1432,7 +1432,7 @@ fn validated_subagent_capsule_from_metadata( validate_subagent_capsule(request, skill_id).map(Some) } -fn completion_subagent_capsule_from_metadata( +pub(crate) fn completion_subagent_capsule_from_metadata( metadata: &Value, ) -> Result, CapsuleValidationError> { let Some(request) = metadata.get(A2A_SUBAGENT_REQUEST_METADATA_PATH) else { @@ -1901,6 +1901,7 @@ async fn complete_a2a_task_with_capsule( return task; } Err(error) => { + warn_a2a_native_turn_failure(&task_id, &context_id, &metadata, &error); let message = a2a_agent_message(&context_id, &error); history.push(message.clone()); attach_server_owned_subagent_completion( @@ -2022,6 +2023,7 @@ fn attach_server_owned_subagent_completion( ) { let Some(capsule) = capsule else { maybe_attach_a2a_subagent_work_graph(metadata, task_id, context_id); + stamp_a2a_work_graph_outcome(metadata, observed_outcome); return; }; if let Some(metadata) = metadata.as_object_mut() { @@ -2140,6 +2142,78 @@ fn a2a_native_session_id(context_id: &str, metadata: &Value) -> String { ) } +fn stamp_a2a_work_graph_outcome(metadata: &mut Value, observed_outcome: &str) { + let Some(work_graph) = metadata + .as_object_mut() + .and_then(|metadata| metadata.get_mut("workGraph")) + .and_then(Value::as_object_mut) + else { + return; + }; + work_graph.insert( + "state".to_string(), + Value::String(observed_outcome.to_string()), + ); + work_graph.insert( + "status".to_string(), + Value::String(observed_outcome.to_string()), + ); +} + +pub(crate) fn a2a_vfs_hydration_gap(context_file_count: i64, hydrated_file_count: i64) -> bool { + context_file_count > 0 && hydrated_file_count == 0 +} + +fn a2a_metadata_i64(metadata: &Value, keys: &[&str]) -> i64 { + let Some(object) = metadata.as_object() else { + return 0; + }; + keys.iter() + .find_map(|key| object.get(*key).and_then(Value::as_i64)) + .unwrap_or(0) +} + +fn warn_a2a_native_turn_failure(task_id: &str, context_id: &str, metadata: &Value, error: &str) { + let object = metadata.as_object(); + let agent_run_id = object + .and_then(|object| { + json_string_from_object(object, &["agentRunId", "agent_run_id", "platformRunId"]) + }) + .unwrap_or_default(); + let platform_run_id = object + .and_then(|object| json_string_from_object(object, &["platformRunId", "platform_run_id"])) + .unwrap_or_default(); + let vfs_context_file_count = + a2a_metadata_i64(metadata, &["vfs_context_file_count", "vfsContextFileCount"]); + let vfs_hydrated_file_count = a2a_metadata_i64( + metadata, + &["vfs_hydrated_file_count", "vfsHydratedFileCount"], + ); + tracing::warn!( + target: "maestro.a2a", + event = "a2a_turn_failed", + task_id = %task_id, + context_id = %context_id, + agent_run_id = %agent_run_id, + platform_run_id = %platform_run_id, + vfs_context_file_count, + vfs_hydrated_file_count, + vfs_hydration_gap = a2a_vfs_hydration_gap(vfs_context_file_count, vfs_hydrated_file_count), + error = %error, + "A2A native turn failed" + ); +} + +fn a2a_subagent_request_object(metadata: &Map) -> Option<&Map> { + [ + A2A_SUBAGENT_REQUEST_METADATA_PATH, + "subagent_request", + "subagentRequest", + ] + .into_iter() + .find_map(|key| metadata.get(key).and_then(Value::as_object)) +} + fn maybe_attach_a2a_subagent_work_graph(metadata: &mut Value, task_id: &str, context_id: &str) { let Some(metadata_object) = metadata.as_object_mut() else { return; @@ -2147,10 +2221,7 @@ fn maybe_attach_a2a_subagent_work_graph(metadata: &mut Value, task_id: &str, con if metadata_object.get("workGraph").is_some() { return; } - let Some(subagent_request) = metadata_object - .get(A2A_SUBAGENT_REQUEST_METADATA_PATH) - .and_then(Value::as_object) - else { + let Some(subagent_request) = a2a_subagent_request_object(metadata_object) else { return; }; let capsule = subagent_request.get("capsule").and_then(Value::as_object); @@ -2166,11 +2237,16 @@ fn maybe_attach_a2a_subagent_work_graph(metadata: &mut Value, task_id: &str, con .unwrap_or_else(|| task_id.to_string()); let child_run_id = format!("a2a-task:{task_id}"); let tool_call_id = format!("a2a-subagent-dispatch:{task_id}"); - let correlation_path = if let Some(swarm_id) = swarm_id.as_deref() { + let platform_run_id = + json_string_from_object(metadata_object, &["platformRunId", "platform_run_id"]); + let mut correlation_path = if let Some(swarm_id) = swarm_id.as_deref() { format!("maestro-swarm/{swarm_id}/{work_item_id}/a2a/{task_id}") } else { format!("a2a/{context_id}/{task_id}") }; + if let Some(platform_run_id) = platform_run_id.as_deref() { + correlation_path.push_str(&format!(";platform_agent_run_id={platform_run_id}")); + } metadata_object.insert( "workGraph".to_string(), @@ -2192,6 +2268,7 @@ fn maybe_attach_a2a_subagent_work_graph(metadata: &mut Value, task_id: &str, con "correlationPath": correlation_path, "rawPayloadWithheld": true, "codexSubagents": { + "schemaVersion": CODEX_SUBAGENT_WORK_GRAPH_SCHEMA, "edgeCount": 1, "toolCallIds": [tool_call_id], "childRunIds": [child_run_id], diff --git a/packages/runtime-gateway-rs/src/model_catalog.rs b/packages/runtime-gateway-rs/src/model_catalog.rs index be24f9c2d..342c13cf3 100644 --- a/packages/runtime-gateway-rs/src/model_catalog.rs +++ b/packages/runtime-gateway-rs/src/model_catalog.rs @@ -508,7 +508,7 @@ pub(crate) fn default_model_from_registry(registry: &ModelRegistry) -> ModelInfo pub(crate) fn emergency_default_model() -> ModelInfo { let models = builtin_models(); - let default_id = shared_catalog::default_model_for_provider("openai").unwrap_or("gpt-5.5"); + let default_id = shared_catalog::default_model_for_provider("openai").unwrap_or("gpt-5.6"); models .iter() .find(|model| model.provider == "openai-codex" && model.id == default_id) @@ -668,7 +668,7 @@ mod tests { let model = emergency_default_model(); assert_eq!(model.provider, "openai-codex"); - assert_eq!(model.id, "gpt-5.5"); + assert_eq!(model.id, "gpt-5.6"); assert_eq!(model.api, "openai-codex-app-server"); } @@ -703,7 +703,7 @@ mod tests { let codex = models .iter() - .find(|model| model.provider == "openai-codex" && model.id == "gpt-5.5") + .find(|model| model.provider == "openai-codex" && model.id == "gpt-5.6") .expect("codex app-server mirror of the default model"); assert_eq!(codex.api, "openai-codex-app-server"); assert!(codex.capabilities.reasoning); @@ -712,8 +712,8 @@ mod tests { codex.context_window, shared .iter() - .find(|model| model.provider == "openai" && model.id == "gpt-5.5") - .expect("shared gpt-5.5") + .find(|model| model.provider == "openai" && model.id == "gpt-5.6") + .expect("shared gpt-5.6") .capabilities .context_tokens, ); diff --git a/packages/runtime-gateway-rs/src/tests.rs b/packages/runtime-gateway-rs/src/tests.rs index ce9655b46..8e1a1b239 100644 --- a/packages/runtime-gateway-rs/src/tests.rs +++ b/packages/runtime-gateway-rs/src/tests.rs @@ -9836,6 +9836,10 @@ async fn a2a_completion_attaches_subagent_work_graph_metadata() { task["metadata"]["workGraph"]["correlationPath"], "maestro-swarm/swarm-1/alpha-review/a2a/maestro-task-1" ); + assert_eq!( + task["metadata"]["workGraph"]["codexSubagents"]["schemaVersion"], + CODEX_SUBAGENT_WORK_GRAPH_SCHEMA + ); if let Some(previous_fake) = previous_fake { env::set_var("MAESTRO_A2A_FAKE_RESPONSE", previous_fake); @@ -9844,6 +9848,74 @@ async fn a2a_completion_attaches_subagent_work_graph_metadata() { } } +#[test] +fn a2a_vfs_hydration_gap_matches_context_files_with_zero_hydrated() { + assert!(crate::a2a::a2a_vfs_hydration_gap(4, 0)); + assert!(!crate::a2a::a2a_vfs_hydration_gap(4, 1)); + assert!(!crate::a2a::a2a_vfs_hydration_gap(0, 0)); +} + +#[tokio::test(flavor = "current_thread")] +async fn a2a_failed_turn_attaches_work_graph_with_run_ids() { + let _guard = ENV_LOCK.lock().await; + let previous_fake = env::var("MAESTRO_A2A_FAKE_RESPONSE").ok(); + env::remove_var("MAESTRO_A2A_FAKE_RESPONSE"); + let mut expired = valid_code_writer_capsule(); + expired["capsule"]["deadlineAt"] = serde_json::json!("2000-01-01T00:00:00Z"); + let state = test_app_state_with_sessions(HashMap::new()); + let (_cancel_tx, cancel_rx) = watch::channel(false); + + let metadata = serde_json::json!({ + A2A_SUBAGENT_REQUEST_METADATA_PATH: expired, + "platformRunId": "platform-run-9047", + "agentRunId": "agent-run-9047", + "vfs_context_file_count": 4, + "vfs_hydrated_file_count": 0 + }); + // Check the same decoder used by completion before executing: an absent + // capsule would select an ordinary native turn and could contact a provider. + let capsule = crate::a2a::completion_subagent_capsule_from_metadata(&metadata) + .expect("valid completion capsule") + .expect("canonical governed capsule is present"); + assert!(crate::a2a::build_a2a_subagent_execution_policy_for_state(&state, &capsule).is_err()); + let task = tokio::time::timeout( + Duration::from_secs(5), + complete_a2a_task( + &state, + "implement this".to_string(), + "failed-task-9047".to_string(), + "ctx-9047".to_string(), + Vec::new(), + metadata, + cancel_rx, + ), + ) + .await + .expect("missing execution policy must fail before provider execution"); + if let Some(previous_fake) = previous_fake { + env::set_var("MAESTRO_A2A_FAKE_RESPONSE", previous_fake); + } + assert!( + task.to_string() + .contains("governed task capsule is missing its pre-claim execution policy") + ); + + assert_eq!(task["status"]["state"], "TASK_STATE_FAILED"); + assert_eq!( + task["metadata"]["workGraph"]["codexSubagents"]["schemaVersion"], + CODEX_SUBAGENT_WORK_GRAPH_SCHEMA + ); + assert!( + task["metadata"]["workGraph"]["correlationPath"] + .as_str() + .expect("correlation path") + .contains("platform_agent_run_id=platform-run-9047"), + "work graph correlation path should join the platform run id: {}", + task["metadata"]["workGraph"]["correlationPath"] + ); + assert_eq!(task["metadata"]["workGraph"]["state"], "failed"); +} + #[tokio::test(flavor = "current_thread")] async fn a2a_subagent_completion_requires_independent_review_disposition() { let _guard = ENV_LOCK.lock().await; @@ -12161,7 +12233,7 @@ fn default_model_handles_empty_registry() { let model = default_model_from_registry(®istry); assert_eq!(model.provider, "openai-codex"); - assert_eq!(model.id, "gpt-5.5"); + assert_eq!(model.id, "gpt-5.6"); assert_eq!(model.api, "openai-codex-app-server"); } @@ -13025,7 +13097,7 @@ fn emergency_default_model_is_available_without_registry_entries() { let model = emergency_default_model(); assert_eq!(model.provider, "openai-codex"); - assert_eq!(model.id, "gpt-5.5"); + assert_eq!(model.id, "gpt-5.6"); assert_eq!(model.api, "openai-codex-app-server"); } diff --git a/packages/runtime-rs/src/agent/native/codex.rs b/packages/runtime-rs/src/agent/native/codex.rs index 58d6fcfc8..abb07994f 100644 --- a/packages/runtime-rs/src/agent/native/codex.rs +++ b/packages/runtime-rs/src/agent/native/codex.rs @@ -2,6 +2,57 @@ use super::*; +/// Hosted AgentRun / A2A ids the worker injects into Maestro's environment. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(super) struct HostedCodexTurnCorrelation { + pub agent_run_id: Option, + pub a2a_task_id: Option, +} + +fn env_nonempty(name: &str) -> Option { + std::env::var(name) + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) +} + +pub(super) fn hosted_codex_turn_correlation_from( + lookup: impl Fn(&str) -> Option, +) -> HostedCodexTurnCorrelation { + HostedCodexTurnCorrelation { + agent_run_id: lookup("MAESTRO_AGENT_RUN_ID").or_else(|| lookup("EVALOPS_AGENT_RUN_ID")), + a2a_task_id: lookup("MAESTRO_A2A_TASK_ID") + .or_else(|| lookup("MAESTRO_A2A_DELEGATION_ID")) + .or_else(|| lookup("A2A_TASK_ID")), + } +} + +pub(super) fn hosted_codex_turn_correlation() -> HostedCodexTurnCorrelation { + hosted_codex_turn_correlation_from(env_nonempty) +} + +/// Connector mounts listed context files but hydration wrote none of them. +#[allow(dead_code)] +pub(super) fn vfs_hydration_yielded_no_files( + context_file_count: i64, + hydrated_file_count: i64, +) -> bool { + context_file_count > 0 && hydrated_file_count == 0 +} + +fn warn_codex_turn_failed(event: &'static str, thread_id: &str, turn_id: &str) { + let correlation = hosted_codex_turn_correlation(); + tracing::warn!( + target: "maestro.codex", + event, + thread_id = %thread_id, + turn_id = %turn_id, + agent_run_id = correlation.agent_run_id.as_deref().unwrap_or(""), + a2a_task_id = correlation.a2a_task_id.as_deref().unwrap_or(""), + "Codex turn failed" + ); +} + impl NativeAgentRunner { /// Ensure a Codex app-server thread exists for `openai-codex/*`. pub(super) async fn ensure_codex_session(&mut self) -> Result<()> { @@ -257,11 +308,10 @@ impl NativeAgentRunner { usage: usage.clone(), }); if let Some(failure) = result.provider_failure().map(str::to_owned) { - tracing::warn!( - target: "maestro.codex", - event = "codex_turn_failed", - thread_id = %result.thread_id, - turn_id = %result.turn_id, + warn_codex_turn_failed( + "codex_turn_failed", + &result.thread_id, + &result.turn_id, ); let _ = self.event_tx.send(FromAgent::CodexTurnState { state: "failed".to_owned(), @@ -271,13 +321,17 @@ impl NativeAgentRunner { return Err(anyhow::anyhow!(failure)); } if final_text.trim().is_empty() { + let correlation = hosted_codex_turn_correlation(); tracing::warn!( target: "maestro.codex", event = "codex_turn_empty_assistant_response", thread_id = %result.thread_id, turn_id = %result.turn_id, + agent_run_id = correlation.agent_run_id.as_deref().unwrap_or(""), + a2a_task_id = correlation.a2a_task_id.as_deref().unwrap_or(""), assistant_text_chars = result.assistant_text.chars().count(), assistant_text_is_full = result.assistant_text_is_full, + "Codex turn failed" ); let _ = self.event_tx.send(FromAgent::CodexTurnState { state: "failed".to_owned(), @@ -1203,3 +1257,26 @@ impl NativeAgentRunner { }); } } + +#[cfg(test)] +mod hosted_turn_diagnostics_tests { + use super::vfs_hydration_yielded_no_files; + + #[test] + fn vfs_hydration_gap_is_context_files_with_zero_hydrated() { + assert!(vfs_hydration_yielded_no_files(4, 0)); + assert!(!vfs_hydration_yielded_no_files(4, 1)); + assert!(!vfs_hydration_yielded_no_files(0, 0)); + } + + #[test] + fn hosted_correlation_reads_agent_run_and_a2a_task_ids() { + let correlation = super::hosted_codex_turn_correlation_from(|name| match name { + "MAESTRO_AGENT_RUN_ID" => Some("run-9047".to_string()), + "MAESTRO_A2A_TASK_ID" => Some("task-9047".to_string()), + _ => None, + }); + assert_eq!(correlation.agent_run_id.as_deref(), Some("run-9047")); + assert_eq!(correlation.a2a_task_id.as_deref(), Some("task-9047")); + } +} diff --git a/packages/runtime-rs/src/agent/retry.rs b/packages/runtime-rs/src/agent/retry.rs index 8b66119bd..e8e049530 100644 --- a/packages/runtime-rs/src/agent/retry.rs +++ b/packages/runtime-rs/src/agent/retry.rs @@ -160,6 +160,8 @@ impl ErrorKind { || lower.contains("usagelimitexceeded") || lower.contains("quota exceeded") || lower.contains("quota_exceeded") + || lower.contains("insufficient_quota") + || lower.contains("prepaid_credits_insufficient") || lower.contains("hit your usage limit") { return ErrorKind::QuotaExceeded; @@ -695,6 +697,25 @@ mod tests { assert!(!ErrorKind::Unknown.is_retryable()); } + #[test] + fn insufficient_quota_is_terminal_even_with_http_429_and_retry_copy() { + for message in [ + "HTTP 429: insufficient_quota: Check your plan and billing details, then try again.", + "HTTP 402: prepaid_credits_insufficient: Add model credits to continue.", + ] { + let kind = ErrorKind::classify(message); + assert_eq!(kind, ErrorKind::QuotaExceeded, "{message}"); + assert!(matches!( + RetryPolicy::default().should_retry(kind), + RetryDecision::GiveUp { .. } + )); + } + assert!(matches!( + ErrorKind::classify("HTTP 429: rate_limit_exceeded: retry after 2 seconds"), + ErrorKind::RateLimited { .. } + )); + } + #[test] fn usage_limit_copy_is_quota_not_transient_try_again() { let kind = ErrorKind::classify( diff --git a/packages/swarm-rs/src/types.rs b/packages/swarm-rs/src/types.rs index dcf323b49..59e4cd9d9 100644 --- a/packages/swarm-rs/src/types.rs +++ b/packages/swarm-rs/src/types.rs @@ -465,15 +465,15 @@ pub fn model_for_tier(tier: ModelTier, provider: ModelProvider) -> &'static str match (tier, provider) { (ModelTier::Opus, ModelProvider::Anthropic) => "claude-opus-4-6", (ModelTier::Opus, ModelProvider::OpenAi) => "gpt-5.2", - (ModelTier::Opus, ModelProvider::OpenAiCodex) => "gpt-5.5", + (ModelTier::Opus, ModelProvider::OpenAiCodex) => "gpt-5.6", (ModelTier::Opus, ModelProvider::Google) => "gemini-2.0-flash-thinking-exp", (ModelTier::Sonnet, ModelProvider::Anthropic) => "claude-sonnet-4-5-20250929", (ModelTier::Sonnet, ModelProvider::OpenAi) => "gpt-4o", - (ModelTier::Sonnet, ModelProvider::OpenAiCodex) => "gpt-5.4", + (ModelTier::Sonnet, ModelProvider::OpenAiCodex) => "gpt-5.6", (ModelTier::Sonnet, ModelProvider::Google) => "gemini-2.0-flash-exp", (ModelTier::Haiku, ModelProvider::Anthropic) => "claude-haiku-4-5-20251001", (ModelTier::Haiku, ModelProvider::OpenAi) => "gpt-4o-mini", - (ModelTier::Haiku, ModelProvider::OpenAiCodex) => "gpt-5.4-mini", + (ModelTier::Haiku, ModelProvider::OpenAiCodex) => "gpt-5.6", (ModelTier::Haiku, ModelProvider::Google) => "gemini-2.0-flash-lite-exp", } } @@ -515,7 +515,7 @@ fn subagent_dispatch_rule( } SubagentType::Coder => SubagentDispatchRule::explicit( ModelProvider::OpenAiCodex, - "gpt-5.5", + "gpt-5.6", ReasoningEffort::Medium, ), SubagentType::Reviewer | SubagentType::Researcher | SubagentType::BrowserQa => { @@ -548,12 +548,12 @@ fn subagent_dispatch_rule( } SubagentType::Coder => SubagentDispatchRule::explicit( ModelProvider::OpenAiCodex, - "gpt-5.5", + "gpt-5.6", ReasoningEffort::High, ), SubagentType::Reviewer => SubagentDispatchRule::explicit( ModelProvider::OpenAiCodex, - "gpt-5.5", + "gpt-5.6", ReasoningEffort::Medium, ), SubagentType::BrowserQa => { diff --git a/packages/swarm-rs/src/types/tests.rs b/packages/swarm-rs/src/types/tests.rs index d624eeea0..1c1d9a16a 100644 --- a/packages/swarm-rs/src/types/tests.rs +++ b/packages/swarm-rs/src/types/tests.rs @@ -377,7 +377,7 @@ fn test_smart_coder_subagent_dispatch_uses_openai_codex_model() { assert_eq!(dispatch.mode, AgentMode::Smart); assert_eq!(dispatch.subagent_type, SubagentType::Coder); assert_eq!(dispatch.provider, ModelProvider::OpenAiCodex); - assert_eq!(dispatch.model, "gpt-5.5"); + assert_eq!(dispatch.model, "gpt-5.6"); assert_eq!(dispatch.reasoning_effort, ReasoningEffort::Medium); assert_eq!(dispatch.source, DispatchSource::Mode); } diff --git a/packages/tui-rs/docs/user-guide/02-authentication.md b/packages/tui-rs/docs/user-guide/02-authentication.md index 050a99cd1..7e6cbfe75 100644 --- a/packages/tui-rs/docs/user-guide/02-authentication.md +++ b/packages/tui-rs/docs/user-guide/02-authentication.md @@ -59,7 +59,7 @@ Check whether the selected Codex profile can accept a prompt: ```bash maestro codex ready -maestro codex ready --profile work --model openai-codex/gpt-5.5 --json +maestro codex ready --profile work --model openai-codex/gpt-5.6 --json ``` `ready` exits 0 when auth, required app-server methods, required notifications, @@ -69,7 +69,7 @@ as optional degraded support and does not fail readiness. Use `--model MODEL` when checking a non-default Codex model. The binding check uses the same canonical model id that runtime sends to `thread/start`, so -`maestro codex ready --model openai-codex/gpt-5.4` checks the `gpt-5.4` binding +`maestro codex ready --model openai-codex/gpt-5.5-pro` checks the `gpt-5.5-pro` binding instead of the default model binding. Common Codex auth commands: diff --git a/packages/tui-rs/src/app/input_handlers.rs b/packages/tui-rs/src/app/input_handlers.rs index 0224f083b..33e2c4c3f 100644 --- a/packages/tui-rs/src/app/input_handlers.rs +++ b/packages/tui-rs/src/app/input_handlers.rs @@ -1610,7 +1610,7 @@ impl App { match crate::connections_cli::save_local_api_key(provider_id, secret) { Ok(connection_id) => { let default_model = crate::model_catalog::default_model_for_provider(provider_id) - .unwrap_or("gpt-5.5"); + .unwrap_or("gpt-5.6"); let route = crate::config::compose_model_route(Some(provider_id), Some(default_model)) .unwrap_or_else(|| format!("{provider_id}/{default_model}")); diff --git a/packages/tui-rs/src/components/message.rs b/packages/tui-rs/src/components/message.rs index 84ab9239d..b3e0a2ced 100644 --- a/packages/tui-rs/src/components/message.rs +++ b/packages/tui-rs/src/components/message.rs @@ -1265,9 +1265,9 @@ pub(crate) fn composer_editor_width(area_width: u16) -> u16 { /// Short label for composer/status chrome. /// -/// Uses the catalog `name` for the current model (`openai-codex/gpt-5.5` → -/// `GPT-5.5`). Unknown ids fall back to the last path segment so the footer -/// is never `openai-codex/gpt-5.5 via openai-codex`. +/// Uses the catalog `name` for the current model (`openai-codex/gpt-5.6` → +/// `GPT-5.6`). Unknown ids fall back to the last path segment so the footer +/// is never `openai-codex/gpt-5.6 via openai-codex`. #[must_use] fn chrome_model_label(model: &str) -> String { if let Some(info) = crate::model_catalog::find_model(model) { @@ -2701,7 +2701,7 @@ impl Widget for ChatView<'_> { }); // Model + mode already sit on the composer border. Passing them - // here reprints `GPT-5.5 via openai-codex` on the next row. + // here reprints `GPT-5.6 via openai-codex` on the next row. let status_widget = StatusBarWidget::new( None, None, @@ -3525,7 +3525,7 @@ mod tests { }, ) .with_runtime_footer( - Some("gpt-5.4"), + Some("gpt-5.6"), ThinkingLevel::High, InteractionMode::AlwaysApprove, ); @@ -3536,7 +3536,7 @@ mod tests { widget.render(Rect::new(0, 0, width, height), &mut buf); let rendered = buffer_lines(&buf, width, height).join("\n"); - assert!(rendered.contains("Mode: Auto-approve · GPT-5.4 (high)")); + assert!(rendered.contains("Mode: Auto-approve · GPT-5.6 (high)")); } #[test] @@ -3551,7 +3551,7 @@ mod tests { }, ) .with_runtime_footer( - Some("openai-codex/gpt-5.5"), + Some("openai-codex/gpt-5.6"), ThinkingLevel::Off, InteractionMode::Normal, ); @@ -3562,14 +3562,14 @@ mod tests { widget.render(Rect::new(0, 0, width, height), &mut buf); let rendered = buffer_lines(&buf, width, height).join("\n"); - assert!(rendered.contains("Mode: Act · GPT-5.5")); - assert!(!rendered.contains("openai-codex/gpt-5.5")); + assert!(rendered.contains("Mode: Act · GPT-5.6")); + assert!(!rendered.contains("openai-codex/gpt-5.6")); } #[test] fn chat_view_shows_model_once_in_startup_summary() { let mut state = crate::state::AppState::default(); - state.model = Some("openai-codex/gpt-5.5".to_string()); + state.model = Some("openai-codex/gpt-5.6".to_string()); state.provider = Some("openai-codex".to_string()); let width = 100; let height = 16; @@ -3578,12 +3578,12 @@ mod tests { ChatView::new(&state).render(Rect::new(0, 0, width, height), &mut buf); let rendered = buffer_lines(&buf, width, height).join("\n"); - let catalog_hits = rendered.matches("GPT-5.5").count(); + let catalog_hits = rendered.matches("GPT-5.6").count(); assert_eq!(catalog_hits, 1, "model must appear once:\n{rendered}"); assert!(rendered.contains("Mode: Act")); assert!(rendered.contains("/plan to plan")); assert!(!rendered.contains("via openai-codex")); - assert!(!rendered.contains("openai-codex/gpt-5.5")); + assert!(!rendered.contains("openai-codex/gpt-5.6")); assert!(!rendered.contains("Describe what you want to build...")); } @@ -3623,8 +3623,8 @@ mod tests { #[test] fn chrome_model_label_inherits_catalog_name() { - assert_eq!(chrome_model_label("openai-codex/gpt-5.5"), "GPT-5.5"); - assert_eq!(chrome_model_label("gpt-5.5"), "GPT-5.5"); + assert_eq!(chrome_model_label("openai-codex/gpt-5.6"), "GPT-5.6"); + assert_eq!(chrome_model_label("gpt-5.6"), "GPT-5.6"); assert_eq!( chrome_model_label("openrouter/openai/gpt-4o-mini"), "OpenAI: GPT-4o-mini" @@ -3972,7 +3972,7 @@ mod tests { fn empty_chat_view_uses_live_runtime_summary() { let mut state = crate::state::AppState::default(); state.session_id = Some("restored-42".to_string()); - state.model = Some("openai-codex/gpt-5.5".to_string()); + state.model = Some("openai-codex/gpt-5.6".to_string()); state.cwd = Some("/projects/release-checklist".to_string()); let width = 100; let height = 20; @@ -3981,7 +3981,7 @@ mod tests { ChatView::new(&state).render(Rect::new(0, 0, width, height), &mut buf); let rendered = buffer_lines(&buf, width, height).join("\n"); - assert!(rendered.contains("GPT-5.5")); + assert!(rendered.contains("GPT-5.6")); assert!(rendered.contains("release-checklist")); assert!(!rendered.contains("session restored-42")); } diff --git a/packages/tui-rs/src/components/model_selector.rs b/packages/tui-rs/src/components/model_selector.rs index 01340dd5c..a4298d595 100644 --- a/packages/tui-rs/src/components/model_selector.rs +++ b/packages/tui-rs/src/components/model_selector.rs @@ -781,7 +781,10 @@ impl ModelSelector { let mut spans = vec![ Span::styled(&model.name, Style::default().add_modifier(Modifier::BOLD)), - Span::styled(format!(" ({}) ", model.provider), theme.muted_style()), + Span::styled( + format!(" ({}) ", model_route_summary(model)), + theme.muted_style(), + ), ]; if is_current { @@ -821,6 +824,24 @@ fn format_context_window(context_tokens: u32) -> String { } } +fn model_route_summary(model: &ModelInfo) -> String { + if let Some(managed) = crate::model_catalog::MANAGED_MODELS + .iter() + .find(|entry| model.provider == "evalops" && entry.id == model.id) + { + let provider = match managed.provider { + "fireworks" => "Fireworks", + "google" => "Google", + other => other, + }; + return format!( + "{provider} · {}", + maestro_ui::localization::tr("Model credits") + ); + } + model.provider.clone() +} + fn model_status_summary(model: &ModelInfo) -> &'static str { use crate::model_catalog::VerificationState; match (model.verification.source.as_str(), model.verification.state) { @@ -858,8 +879,39 @@ fn capability_summary(model: &ModelInfo) -> String { mod tests { use super::*; #[test] - fn managed_fireworks_search_selects_exact_gateway_routes() { - for model in crate::model_catalog::MANAGED_FIREWORKS_MODELS { + fn managed_model_picker_shows_funding_and_upstream_without_changing_route() { + use ratatui::{Terminal, backend::TestBackend}; + for (id, provider) in [ + ("gemini-3.8-flash", "Google"), + ("accounts/fireworks/models/qwen3p8-max", "Fireworks"), + ] { + let mut selector = ModelSelector::new(); + selector.show(); + let route = format!("evalops/{id}"); + selector.insert_str(&route); + let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap(); + terminal + .draw(|frame| selector.render(frame, frame.area())) + .unwrap(); + let text: String = terminal + .backend() + .buffer() + .content + .iter() + .map(|cell| cell.symbol()) + .collect(); + assert!( + text.contains(&format!("{provider} · Model credits")), + "{text}" + ); + assert!(text.contains("availability unchecked"), "{text}"); + assert_eq!(selector.confirm(), Some(route)); + } + } + + #[test] + fn managed_model_search_selects_exact_gateway_routes() { + for model in crate::model_catalog::MANAGED_MODELS { let mut selector = ModelSelector::new(); selector.set_current_model(Some( crate::credential_mode::DEFAULT_MANAGED_MODEL.to_owned(), @@ -1243,8 +1295,9 @@ mod tests { } } - /// The real provider defaults plus filler models, so the focused - /// slice exercises `default_model_for_provider` against known ids. + /// Provider defaults plus filler models. OpenAI's catalog default is also + /// a preferred discovery row; tests that pin slice order keep a non-default + /// OpenAI id here and add `gpt-5.6` only where that default is under test. fn slice_catalog() -> Vec { let mut models = vec![ test_model("claude-sonnet-4-6", "anthropic"), @@ -1261,7 +1314,9 @@ mod tests { #[test] fn focused_slice_shows_current_and_provider_defaults() { - let mut selector = ModelSelector::with_models(slice_catalog()); + let mut models = slice_catalog(); + models.push(test_model("gpt-5.6", "openai")); + let mut selector = ModelSelector::with_models(models); selector.set_current_model(Some("grok-4.5".to_owned())); selector.show(); @@ -1270,10 +1325,21 @@ mod tests { .iter() .map(|&idx| selector.models[idx].id.as_str()) .collect(); - assert_eq!(ids[0], "grok-4.5", "current model leads the slice"); - for default in ["claude-sonnet-4-6", "gemini-2.5-pro", "gpt-5.5"] { + assert_eq!( + ids[0], "gpt-5.6", + "preferred openai default leads the slice" + ); + assert!( + ids.contains(&"grok-4.5"), + "current model stays in the slice" + ); + for default in ["claude-sonnet-4-6", "gemini-2.5-pro", "gpt-5.6"] { assert!(ids.contains(&default), "slice must include {default}"); } + assert!( + !ids.contains(&"gpt-5.5"), + "retired openai default is not a focused-slice row" + ); let default_providers: Vec<&str> = selector .filtered .iter() diff --git a/packages/tui-rs/src/onboarding_checks.rs b/packages/tui-rs/src/onboarding_checks.rs index 4eacdc266..1defd4b6a 100644 --- a/packages/tui-rs/src/onboarding_checks.rs +++ b/packages/tui-rs/src/onboarding_checks.rs @@ -250,7 +250,7 @@ async fn managed_policy_check(model: &str, cwd: &Path) -> OnboardingCheck { // BYOK is not an exemption from the workspace's managed setup policy. let (_mode, identity) = crate::credential_mode::require_ready_with_identity(&model)?; let policy = crate::managed_setup::ManagedSetupClient::resolve_with( - Some(&identity), + identity.as_ref(), None, 0, Duration::ZERO, diff --git a/packages/ui-rs/src/translations.rs b/packages/ui-rs/src/translations.rs index 518d1d684..603a66522 100644 --- a/packages/ui-rs/src/translations.rs +++ b/packages/ui-rs/src/translations.rs @@ -13244,6 +13244,17 @@ pub(crate) const MESSAGES: &[(&str, [&str; 6])] = &[ "模型和思考强度", ], ), + ( + "Model credits", + [ + "Créditos para modelos", + "Crédits de modèles", + "Modellguthaben", + "モデルクレジット", + "모델 크레딧", + "模型额度", + ], + ), ( "Model name", [ diff --git a/scripts/hosted-orb-delegation-acceptance.test.mjs b/scripts/hosted-orb-delegation-acceptance.test.mjs index ada78db76..85b2c8081 100644 --- a/scripts/hosted-orb-delegation-acceptance.test.mjs +++ b/scripts/hosted-orb-delegation-acceptance.test.mjs @@ -57,25 +57,3 @@ test("the public validator rejects live-gate metadata", async () => { contract.live_gate = { enabled: true }; assert.throws(() => validateContract(contract), /live-gate inventory/); }); - -test("the managed hosted acceptance is a hard gate when explicitly configured", async () => { - const workflow = await readFile( - new URL("../.github/workflows/maestro-ci.yml", import.meta.url), - "utf8", - ); - const block = - workflow.split(" hosted-orb-delegation-live:")[1]?.split(/\n [a-z]/)[0] ?? - ""; - assert.match(block, /Optional hosted Orb acceptance was not requested; skipping\./); - assert.match(block, /npm run smoke:hosted-orb-delegation/); - assert.match( - block, - /if: \(github\.event_name != 'pull_request' \|\| github\.event\.pull_request\.head\.repo\.full_name == github\.repository\) && \(github\.event_name == 'schedule' \|\| github\.event_name == 'workflow_dispatch'\)/, - ); - assert.match(workflow, /vars\.MAESTRO_HOSTED_ORB_LIVE_SMOKE \|\| ''/); - assert.match( - block, - /continue-on-error:\s*\$\{\{\s*!\(github\.event\.inputs\.hosted_orb_live_smoke == '1' \|\| vars\.MAESTRO_HOSTED_ORB_LIVE_SMOKE == '1'\)\s*\}\}/, - ); - assert.doesNotMatch(block, /^ continue-on-error:\s*true\b/m); -});