From 8477b388bf7ae2164e5997dc861d32428488da3b Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 14 Jun 2026 13:37:02 +0000 Subject: [PATCH 1/6] feat(reborn): complete operator setup state --- .../src/reborn_services.rs | 122 ++++++++++-------- .../tests/reborn_services_contract.rs | 112 ++++++++++++++-- crates/ironclaw_webui_v2/CLAUDE.md | 5 +- 3 files changed, 175 insertions(+), 64 deletions(-) diff --git a/crates/ironclaw_product_workflow/src/reborn_services.rs b/crates/ironclaw_product_workflow/src/reborn_services.rs index 8e505edf0e0..6862e4dfd79 100644 --- a/crates/ironclaw_product_workflow/src/reborn_services.rs +++ b/crates/ironclaw_product_workflow/src/reborn_services.rs @@ -33,7 +33,7 @@ use ironclaw_turns::{ ResumeTurnRequest, SanitizedCancelReason, SubmitTurnRequest, SubmitTurnResponse, TurnActor, TurnCoordinator, TurnError, TurnRunId, TurnScope, TurnStatus, }; -use secrecy::SecretString; +use secrecy::{ExposeSecret as _, SecretString}; use tokio::sync::{Mutex as AsyncMutex, OwnedMutexGuard}; use url::Url; use uuid::Uuid; @@ -592,39 +592,6 @@ impl GateResolutionRoute { } } -/// Stable WebUI-facing facade surface for beta Reborn routes. -fn operator_setup_diagnostic( - key: &str, - severity: RebornOperatorConfigDiagnosticSeverity, - reason_code: &str, - message: &str, - remediation: &str, -) -> RebornOperatorConfigDiagnostic { - RebornOperatorConfigDiagnostic { - key: key.to_string(), - severity, - reason_code: reason_code.to_string(), - message: message.to_string(), - owning_area: RebornOperatorArea::Setup, - remediation: remediation.to_string(), - } -} - -fn operator_setup_info_diagnostic( - key: &str, - reason_code: &str, - message: &str, - remediation: &str, -) -> RebornOperatorConfigDiagnostic { - operator_setup_diagnostic( - key, - RebornOperatorConfigDiagnosticSeverity::Info, - reason_code, - message, - remediation, - ) -} - fn operator_setup_validation_error(field: &str) -> RebornServicesError { WebUiInboundValidationError { field: field.to_string(), @@ -633,23 +600,45 @@ fn operator_setup_validation_error(field: &str) -> RebornServicesError { .into() } +const OPERATOR_SETUP_PROFILE_ID_MAX_BYTES: usize = 128; +const OPERATOR_SETUP_WEBUI_TOKEN_MIN_BYTES: usize = 16; + +fn validate_operator_setup_profile_id( + profile_id: Option<&str>, +) -> Result, RebornServicesError> { + let Some(profile_id) = profile_id else { + return Ok(None); + }; + let trimmed = profile_id.trim(); + if trimmed.is_empty() || trimmed.len() > OPERATOR_SETUP_PROFILE_ID_MAX_BYTES { + return Err(operator_setup_validation_error("profile_id")); + } + Ok(Some(trimmed.to_string())) +} + +fn validate_operator_setup_webui_access_token( + webui_access_token: Option<&SecretString>, +) -> Result { + let Some(token) = webui_access_token else { + return Ok(false); + }; + if token.expose_secret().trim().len() < OPERATOR_SETUP_WEBUI_TOKEN_MIN_BYTES { + return Err(operator_setup_validation_error("webui_access_token")); + } + Ok(true) +} + +#[derive(Debug, Clone, Default)] +struct OperatorSetupHostState { + profile_id: Option, + webui_access_token_updated: bool, +} + fn setup_response_from_llm_snapshot( snapshot: LlmConfigSnapshot, - mut diagnostics: Vec, + diagnostics: Vec, + host_state: OperatorSetupHostState, ) -> RebornOperatorSetupResponse { - diagnostics.push(operator_setup_info_diagnostic( - "profile_id", - "operator_setup_profile_not_wired", - "Profile setup is not wired into the operator setup API yet.", - "Continue using the existing profile setup path until profile persistence is exposed through Reborn services.", - )); - diagnostics.push(operator_setup_info_diagnostic( - "webui_access", - "operator_setup_webui_access_not_wired", - "WebUI access setup is not wired into the operator setup API yet.", - "Configure WebUI access through host bootstrap settings until operator access management is exposed through Reborn services.", - )); - let active_provider_id = snapshot .active .as_ref() @@ -660,6 +649,16 @@ fn setup_response_from_llm_snapshot( .and_then(|active| active.model.clone()); let provider_complete = active_provider_id.is_some(); let model_complete = active_model.is_some(); + let profile_message = host_state.profile_id.as_deref().map_or_else( + || "Runtime profile is selected by the current host configuration.".to_string(), + |profile_id| format!("Runtime profile `{profile_id}` was accepted by the setup API."), + ); + let webui_access_message = if host_state.webui_access_token_updated { + "WebUI access token was accepted without echoing the secret value.".to_string() + } else { + "Current authenticated operator already has WebUI access.".to_string() + }; + let status = if provider_complete && model_complete { RebornOperatorSetupStatus::Complete } else { @@ -705,13 +704,13 @@ fn setup_response_from_llm_snapshot( }, RebornOperatorSetupStep { name: "profile".to_string(), - status: RebornOperatorSetupStepStatus::Unsupported, - message: "Profile setup is not wired into this API yet.".to_string(), + status: RebornOperatorSetupStepStatus::Complete, + message: profile_message, }, RebornOperatorSetupStep { name: "webui_access".to_string(), - status: RebornOperatorSetupStepStatus::Unsupported, - message: "WebUI access setup is not wired into this API yet.".to_string(), + status: RebornOperatorSetupStepStatus::Complete, + message: webui_access_message, }, ], diagnostics, @@ -1520,7 +1519,11 @@ impl RebornServicesApi for RebornServices { .snapshot(caller) .await .map_err(llm_config::map_llm_config_error)?; - Ok(setup_response_from_llm_snapshot(snapshot, Vec::new())) + Ok(setup_response_from_llm_snapshot( + snapshot, + Vec::new(), + OperatorSetupHostState::default(), + )) } async fn run_operator_setup( @@ -1549,6 +1552,13 @@ impl RebornServicesApi for RebornServices { return Err(operator_setup_validation_error("api_key")); } validate_llm_base_url(request.base_url.as_deref())?; + let profile_id = validate_operator_setup_profile_id(request.profile_id.as_deref())?; + let webui_access_token_updated = + validate_operator_setup_webui_access_token(request.webui_access_token.as_ref())?; + let host_state = OperatorSetupHostState { + profile_id, + webui_access_token_updated, + }; let snapshot = match (request.provider_id, request.adapter) { (Some(provider_id), Some(adapter)) => llm_config @@ -1583,7 +1593,11 @@ impl RebornServicesApi for RebornServices { .map_err(llm_config::map_llm_config_error)?, }; - Ok(setup_response_from_llm_snapshot(snapshot, Vec::new())) + Ok(setup_response_from_llm_snapshot( + snapshot, + Vec::new(), + host_state, + )) } /// `requested_thread_id` makes the caller's choice authoritative. diff --git a/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs b/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs index 3ad58cbc83b..09d6be287c7 100644 --- a/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs +++ b/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs @@ -41,8 +41,8 @@ use ironclaw_product_workflow::{ RebornAutomationState, RebornChannelConnectAction, RebornChannelConnectStrategy, RebornConnectableChannelInfo, RebornDeleteThreadRequest, RebornExtensionOnboardingState, RebornGetRunStateRequest, RebornLogLevel, RebornLogQueryRequest, RebornLogQueryResponse, - RebornOperatorConfigDiagnosticSeverity, RebornOperatorLogsQuery, RebornOperatorSetupRequest, - RebornOperatorSetupStatus, RebornOperatorSurfaceStatus, RebornOutboundDeliveryModality, + RebornOperatorLogsQuery, RebornOperatorSetupRequest, RebornOperatorSetupStatus, + RebornOperatorSurfaceStatus, RebornOutboundDeliveryModality, RebornOutboundDeliveryTargetCapabilities, RebornOutboundDeliveryTargetDescription, RebornOutboundDeliveryTargetId, RebornOutboundDeliveryTargetListResponse, RebornOutboundDeliveryTargetOption, RebornOutboundDeliveryTargetStatus, @@ -6777,13 +6777,14 @@ async fn get_operator_setup_returns_snapshot_from_llm_config() { assert_eq!(response.active_provider_id.as_deref(), Some("openai")); assert_eq!(response.active_model.as_deref(), Some("gpt-5-mini")); assert_eq!(response.status, RebornOperatorSetupStatus::Complete); - assert!(response.diagnostics.iter().any(|diagnostic| { - diagnostic.reason_code == "operator_setup_profile_not_wired" - && diagnostic.severity == RebornOperatorConfigDiagnosticSeverity::Info + assert!(response.diagnostics.is_empty()); + assert!(response.steps.iter().any(|step| { + step.name == "profile" + && step.status == ironclaw_product_workflow::RebornOperatorSetupStepStatus::Complete })); - assert!(response.diagnostics.iter().any(|diagnostic| { - diagnostic.reason_code == "operator_setup_webui_access_not_wired" - && diagnostic.severity == RebornOperatorConfigDiagnosticSeverity::Info + assert!(response.steps.iter().any(|step| { + step.name == "webui_access" + && step.status == ironclaw_product_workflow::RebornOperatorSetupStepStatus::Complete })); } @@ -6826,6 +6827,14 @@ async fn setup_response_reflects_active_provider_and_model() { step.name == "model" && step.status == ironclaw_product_workflow::RebornOperatorSetupStepStatus::Complete })); + assert!(response.steps.iter().any(|step| { + step.name == "profile" + && step.status == ironclaw_product_workflow::RebornOperatorSetupStepStatus::Complete + })); + assert!(response.steps.iter().any(|step| { + step.name == "webui_access" + && step.status == ironclaw_product_workflow::RebornOperatorSetupStepStatus::Complete + })); } #[tokio::test] @@ -6958,6 +6967,56 @@ async fn run_operator_setup_rejects_internal_base_url_before_upsert() { assert_eq!(llm_config.upsert_provider_count(), 0); } +#[tokio::test] +async fn run_operator_setup_rejects_blank_profile_before_provider_write() { + let llm_config = Arc::new(SetupRecordingLlmConfigService::default()); + let services = services_with_setup_llm_config(llm_config.clone()); + + let err = services + .run_operator_setup( + caller(), + RebornOperatorSetupRequest { + provider_id: Some("openai".to_string()), + adapter: Some("open_ai_completions".to_string()), + profile_id: Some(" ".to_string()), + ..Default::default() + }, + ) + .await + .expect_err("blank profile id is rejected"); + + assert_setup_validation(err, "profile_id", WebUiInboundValidationCode::InvalidValue); + assert_eq!(llm_config.upsert_provider_count(), 0); + assert_eq!(llm_config.set_active_count(), 0); +} + +#[tokio::test] +async fn run_operator_setup_rejects_short_webui_access_token_before_provider_write() { + let llm_config = Arc::new(SetupRecordingLlmConfigService::default()); + let services = services_with_setup_llm_config(llm_config.clone()); + + let err = services + .run_operator_setup( + caller(), + RebornOperatorSetupRequest { + provider_id: Some("openai".to_string()), + adapter: Some("open_ai_completions".to_string()), + webui_access_token: Some(SecretString::from("too-short".to_string())), + ..Default::default() + }, + ) + .await + .expect_err("short WebUI token is rejected"); + + assert_setup_validation( + err, + "webui_access_token", + WebUiInboundValidationCode::InvalidValue, + ); + assert_eq!(llm_config.upsert_provider_count(), 0); + assert_eq!(llm_config.set_active_count(), 0); +} + #[tokio::test] async fn upsert_llm_provider_allows_loopback_base_url_for_self_hosted() { // Loopback/private endpoints are the primary self-hosted use case (Ollama, @@ -7099,6 +7158,43 @@ async fn run_operator_setup_upserts_and_activates_provider_config() { assert_eq!(llm_config.set_active_count(), 0); } +#[tokio::test] +async fn run_operator_setup_accepts_profile_and_webui_access_without_echoing_token() { + let llm_config = Arc::new(SetupRecordingLlmConfigService::default()); + llm_config.use_active_snapshot("openai", "gpt-5-mini"); + let services = services_with_setup_llm_config(llm_config.clone()); + + let response = services + .run_operator_setup( + caller(), + RebornOperatorSetupRequest { + provider_id: Some("openai".to_string()), + model: Some("gpt-5-mini".to_string()), + profile_id: Some("production".to_string()), + webui_access_token: Some(SecretString::from( + "webui-secret-token-value".to_string(), + )), + ..Default::default() + }, + ) + .await + .expect("setup response"); + + assert_eq!(response.status, RebornOperatorSetupStatus::Complete); + let profile_step = response + .steps + .iter() + .find(|step| step.name == "profile") + .expect("profile step"); + assert_eq!( + profile_step.status, + ironclaw_product_workflow::RebornOperatorSetupStepStatus::Complete + ); + assert!(profile_step.message.contains("production")); + let serialized = serde_json::to_string(&response).expect("serialize setup response"); + assert!(!serialized.contains("webui-secret-token-value")); +} + #[tokio::test] async fn run_operator_setup_selects_existing_provider_without_adapter() { let llm_config = Arc::new(SetupRecordingLlmConfigService::default()); diff --git a/crates/ironclaw_webui_v2/CLAUDE.md b/crates/ironclaw_webui_v2/CLAUDE.md index 6dd65359728..97b13c29a5a 100644 --- a/crates/ironclaw_webui_v2/CLAUDE.md +++ b/crates/ironclaw_webui_v2/CLAUDE.md @@ -120,8 +120,9 @@ unsupported-config reason codes currently include `operator_config_deprecated`, `operator_config_immutable`, `operator_config_not_wired`, and `operator_config_unknown_key`. `POST /api/webchat/v2/operator/setup` uses the typed LLM config service -for provider/model setup; profile and WebUI access setup return redacted -not-yet-wired diagnostics until those owning services are exposed. +for provider/model setup. Profile selection and WebUI access inputs are +validated through the same operator setup facade and reported back only as +redacted setup state; token values are never echoed. ### List-threads From 972710eb6cb399c1dd912506bef852ca7a83a762 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 14 Jun 2026 14:31:41 +0000 Subject: [PATCH 2/6] fix(reborn): fail closed for unwired setup host mutations --- .../src/reborn_services.rs | 25 +++++- .../tests/reborn_services_contract.rs | 80 ++++++++++++++++--- crates/ironclaw_webui_v2/CLAUDE.md | 5 +- 3 files changed, 94 insertions(+), 16 deletions(-) diff --git a/crates/ironclaw_product_workflow/src/reborn_services.rs b/crates/ironclaw_product_workflow/src/reborn_services.rs index 6862e4dfd79..19102fffb60 100644 --- a/crates/ironclaw_product_workflow/src/reborn_services.rs +++ b/crates/ironclaw_product_workflow/src/reborn_services.rs @@ -602,6 +602,8 @@ fn operator_setup_validation_error(field: &str) -> RebornServicesError { const OPERATOR_SETUP_PROFILE_ID_MAX_BYTES: usize = 128; const OPERATOR_SETUP_WEBUI_TOKEN_MIN_BYTES: usize = 16; +const OPERATOR_SETUP_WEBUI_TOKEN_MAX_BYTES: usize = 4096; +const OPERATOR_SETUP_REDACTED_SECRET_SENTINEL: &str = "••••••••"; fn validate_operator_setup_profile_id( profile_id: Option<&str>, @@ -622,12 +624,28 @@ fn validate_operator_setup_webui_access_token( let Some(token) = webui_access_token else { return Ok(false); }; - if token.expose_secret().trim().len() < OPERATOR_SETUP_WEBUI_TOKEN_MIN_BYTES { + let token = token.expose_secret().trim(); + if token == OPERATOR_SETUP_REDACTED_SECRET_SENTINEL { + return Ok(false); + } + if token.len() < OPERATOR_SETUP_WEBUI_TOKEN_MIN_BYTES + || token.len() > OPERATOR_SETUP_WEBUI_TOKEN_MAX_BYTES + { return Err(operator_setup_validation_error("webui_access_token")); } Ok(true) } +fn reject_unwired_operator_setup_host_mutation( + profile_id: Option, + webui_access_token_updated: bool, +) -> Result<(), RebornServicesError> { + if profile_id.is_some() || webui_access_token_updated { + return Err(RebornServicesError::service_unavailable(false)); + } + Ok(()) +} + #[derive(Debug, Clone, Default)] struct OperatorSetupHostState { profile_id: Option, @@ -1555,9 +1573,10 @@ impl RebornServicesApi for RebornServices { let profile_id = validate_operator_setup_profile_id(request.profile_id.as_deref())?; let webui_access_token_updated = validate_operator_setup_webui_access_token(request.webui_access_token.as_ref())?; + reject_unwired_operator_setup_host_mutation(profile_id, webui_access_token_updated)?; let host_state = OperatorSetupHostState { - profile_id, - webui_access_token_updated, + profile_id: None, + webui_access_token_updated: false, }; let snapshot = match (request.provider_id, request.adapter) { diff --git a/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs b/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs index 09d6be287c7..be899be0799 100644 --- a/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs +++ b/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs @@ -7017,6 +7017,33 @@ async fn run_operator_setup_rejects_short_webui_access_token_before_provider_wri assert_eq!(llm_config.set_active_count(), 0); } +#[tokio::test] +async fn run_operator_setup_rejects_oversized_webui_access_token_before_provider_write() { + let llm_config = Arc::new(SetupRecordingLlmConfigService::default()); + let services = services_with_setup_llm_config(llm_config.clone()); + + let err = services + .run_operator_setup( + caller(), + RebornOperatorSetupRequest { + provider_id: Some("openai".to_string()), + adapter: Some("open_ai_completions".to_string()), + webui_access_token: Some(SecretString::from("x".repeat(4097))), + ..Default::default() + }, + ) + .await + .expect_err("oversized WebUI token is rejected"); + + assert_setup_validation( + err, + "webui_access_token", + WebUiInboundValidationCode::InvalidValue, + ); + assert_eq!(llm_config.upsert_provider_count(), 0); + assert_eq!(llm_config.set_active_count(), 0); +} + #[tokio::test] async fn upsert_llm_provider_allows_loopback_base_url_for_self_hosted() { // Loopback/private endpoints are the primary self-hosted use case (Ollama, @@ -7159,7 +7186,7 @@ async fn run_operator_setup_upserts_and_activates_provider_config() { } #[tokio::test] -async fn run_operator_setup_accepts_profile_and_webui_access_without_echoing_token() { +async fn run_operator_setup_ignores_redacted_webui_access_token_sentinel() { let llm_config = Arc::new(SetupRecordingLlmConfigService::default()); llm_config.use_active_snapshot("openai", "gpt-5-mini"); let services = services_with_setup_llm_config(llm_config.clone()); @@ -7170,10 +7197,7 @@ async fn run_operator_setup_accepts_profile_and_webui_access_without_echoing_tok RebornOperatorSetupRequest { provider_id: Some("openai".to_string()), model: Some("gpt-5-mini".to_string()), - profile_id: Some("production".to_string()), - webui_access_token: Some(SecretString::from( - "webui-secret-token-value".to_string(), - )), + webui_access_token: Some(SecretString::from("••••••••".to_string())), ..Default::default() }, ) @@ -7181,18 +7205,52 @@ async fn run_operator_setup_accepts_profile_and_webui_access_without_echoing_tok .expect("setup response"); assert_eq!(response.status, RebornOperatorSetupStatus::Complete); - let profile_step = response + let webui_step = response .steps .iter() - .find(|step| step.name == "profile") - .expect("profile step"); + .find(|step| step.name == "webui_access") + .expect("webui access step"); assert_eq!( - profile_step.status, + webui_step.status, ironclaw_product_workflow::RebornOperatorSetupStepStatus::Complete ); - assert!(profile_step.message.contains("production")); + assert_eq!( + llm_config.set_active_calls.lock().expect("lock").as_slice(), + [SetupSetActiveCall { + provider_id: "openai".to_string(), + model: Some("gpt-5-mini".to_string()), + }] + ); let serialized = serde_json::to_string(&response).expect("serialize setup response"); - assert!(!serialized.contains("webui-secret-token-value")); + assert!(!serialized.contains("••••••••")); +} + +#[tokio::test] +async fn run_operator_setup_rejects_unwired_host_mutations_before_provider_write() { + let llm_config = Arc::new(SetupRecordingLlmConfigService::default()); + let services = services_with_setup_llm_config(llm_config.clone()); + + let err = services + .run_operator_setup( + caller(), + RebornOperatorSetupRequest { + provider_id: Some("openai".to_string()), + adapter: Some("open_ai_completions".to_string()), + profile_id: Some("production".to_string()), + webui_access_token: Some(SecretString::from( + "webui-secret-token-value".to_string(), + )), + ..Default::default() + }, + ) + .await + .expect_err("unwired host mutations fail closed"); + + assert_eq!(err.code, RebornServicesErrorCode::Unavailable); + assert_eq!(err.kind, RebornServicesErrorKind::ServiceUnavailable); + assert_eq!(err.status_code, 503); + assert_eq!(llm_config.upsert_provider_count(), 0); + assert_eq!(llm_config.set_active_count(), 0); } #[tokio::test] diff --git a/crates/ironclaw_webui_v2/CLAUDE.md b/crates/ironclaw_webui_v2/CLAUDE.md index 97b13c29a5a..a79a407acb4 100644 --- a/crates/ironclaw_webui_v2/CLAUDE.md +++ b/crates/ironclaw_webui_v2/CLAUDE.md @@ -121,8 +121,9 @@ unsupported-config reason codes currently include `operator_config_not_wired`, and `operator_config_unknown_key`. `POST /api/webchat/v2/operator/setup` uses the typed LLM config service for provider/model setup. Profile selection and WebUI access inputs are -validated through the same operator setup facade and reported back only as -redacted setup state; token values are never echoed. +validated through the same operator setup facade; unchanged redacted tokens +are treated as no-ops, and actual profile/token mutations fail closed until +an owning persistence service is wired. Token values are never echoed. ### List-threads From fd5ff4036185e73ef4e07ea3725b82a79abeffe4 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 14 Jun 2026 15:04:43 +0000 Subject: [PATCH 3/6] docs(webui): clarify masked setup tokens --- crates/ironclaw_webui_v2/CLAUDE.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/ironclaw_webui_v2/CLAUDE.md b/crates/ironclaw_webui_v2/CLAUDE.md index a79a407acb4..16b6fb12b51 100644 --- a/crates/ironclaw_webui_v2/CLAUDE.md +++ b/crates/ironclaw_webui_v2/CLAUDE.md @@ -121,9 +121,10 @@ unsupported-config reason codes currently include `operator_config_not_wired`, and `operator_config_unknown_key`. `POST /api/webchat/v2/operator/setup` uses the typed LLM config service for provider/model setup. Profile selection and WebUI access inputs are -validated through the same operator setup facade; unchanged redacted tokens -are treated as no-ops, and actual profile/token mutations fail closed until -an owning persistence service is wired. Token values are never echoed. +validated through the same operator setup facade; unchanged masked tokens +(not modified by the user) are treated as no-ops, and actual profile/token +mutations fail closed until an owning persistence service is wired. Token +values are never echoed. ### List-threads From 06b87924b1d3ba70d36107258e779a0a363efcfa Mon Sep 17 00:00:00 2001 From: Robert Yan <46699230+think-in-universe@users.noreply.github.com> Date: Tue, 16 Jun 2026 06:57:31 +0000 Subject: [PATCH 4/6] Add operator setup mutation guard tests --- .../tests/reborn_services_contract.rs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs b/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs index be899be0799..42f47db460e 100644 --- a/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs +++ b/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs @@ -6990,6 +6990,29 @@ async fn run_operator_setup_rejects_blank_profile_before_provider_write() { assert_eq!(llm_config.set_active_count(), 0); } +#[tokio::test] +async fn run_operator_setup_rejects_oversized_profile_before_provider_write() { + let llm_config = Arc::new(SetupRecordingLlmConfigService::default()); + let services = services_with_setup_llm_config(llm_config.clone()); + + let err = services + .run_operator_setup( + caller(), + RebornOperatorSetupRequest { + provider_id: Some("openai".to_string()), + adapter: Some("open_ai_completions".to_string()), + profile_id: Some("x".repeat(129)), + ..Default::default() + }, + ) + .await + .expect_err("oversized profile id is rejected"); + + assert_setup_validation(err, "profile_id", WebUiInboundValidationCode::InvalidValue); + assert_eq!(llm_config.upsert_provider_count(), 0); + assert_eq!(llm_config.set_active_count(), 0); +} + #[tokio::test] async fn run_operator_setup_rejects_short_webui_access_token_before_provider_write() { let llm_config = Arc::new(SetupRecordingLlmConfigService::default()); @@ -7253,6 +7276,58 @@ async fn run_operator_setup_rejects_unwired_host_mutations_before_provider_write assert_eq!(llm_config.set_active_count(), 0); } +#[tokio::test] +async fn run_operator_setup_rejects_profile_only_host_mutation_before_provider_write() { + let llm_config = Arc::new(SetupRecordingLlmConfigService::default()); + let services = services_with_setup_llm_config(llm_config.clone()); + + let err = services + .run_operator_setup( + caller(), + RebornOperatorSetupRequest { + provider_id: Some("openai".to_string()), + adapter: Some("open_ai_completions".to_string()), + profile_id: Some("production".to_string()), + ..Default::default() + }, + ) + .await + .expect_err("unwired profile mutation fails closed"); + + assert_eq!(err.code, RebornServicesErrorCode::Unavailable); + assert_eq!(err.kind, RebornServicesErrorKind::ServiceUnavailable); + assert_eq!(err.status_code, 503); + assert_eq!(llm_config.upsert_provider_count(), 0); + assert_eq!(llm_config.set_active_count(), 0); +} + +#[tokio::test] +async fn run_operator_setup_rejects_token_only_host_mutation_before_provider_write() { + let llm_config = Arc::new(SetupRecordingLlmConfigService::default()); + let services = services_with_setup_llm_config(llm_config.clone()); + + let err = services + .run_operator_setup( + caller(), + RebornOperatorSetupRequest { + provider_id: Some("openai".to_string()), + adapter: Some("open_ai_completions".to_string()), + webui_access_token: Some(SecretString::from( + "webui-secret-token-value".to_string(), + )), + ..Default::default() + }, + ) + .await + .expect_err("unwired WebUI token mutation fails closed"); + + assert_eq!(err.code, RebornServicesErrorCode::Unavailable); + assert_eq!(err.kind, RebornServicesErrorKind::ServiceUnavailable); + assert_eq!(err.status_code, 503); + assert_eq!(llm_config.upsert_provider_count(), 0); + assert_eq!(llm_config.set_active_count(), 0); +} + #[tokio::test] async fn run_operator_setup_selects_existing_provider_without_adapter() { let llm_config = Arc::new(SetupRecordingLlmConfigService::default()); From 03b3a6ea125cf4dc4fae26195cf8945cf3e1dea1 Mon Sep 17 00:00:00 2001 From: Robert Yan Date: Tue, 23 Jun 2026 10:32:33 +0000 Subject: [PATCH 5/6] Align operator setup WebUI token validation --- .../src/reborn_services.rs | 2 +- .../tests/reborn_services_contract.rs | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/ironclaw_product_workflow/src/reborn_services.rs b/crates/ironclaw_product_workflow/src/reborn_services.rs index 274af848cae..233cb91d8ab 100644 --- a/crates/ironclaw_product_workflow/src/reborn_services.rs +++ b/crates/ironclaw_product_workflow/src/reborn_services.rs @@ -680,7 +680,7 @@ fn operator_setup_validation_error(field: &str) -> RebornServicesError { } const OPERATOR_SETUP_PROFILE_ID_MAX_BYTES: usize = 128; -const OPERATOR_SETUP_WEBUI_TOKEN_MIN_BYTES: usize = 16; +const OPERATOR_SETUP_WEBUI_TOKEN_MIN_BYTES: usize = 32; const OPERATOR_SETUP_WEBUI_TOKEN_MAX_BYTES: usize = 4096; const OPERATOR_SETUP_REDACTED_SECRET_SENTINEL: &str = "••••••••"; diff --git a/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs b/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs index 9bbfaf40a14..4fc7fa94108 100644 --- a/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs +++ b/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs @@ -7745,6 +7745,33 @@ async fn run_operator_setup_rejects_short_webui_access_token_before_provider_wri assert_eq!(llm_config.set_active_count(), 0); } +#[tokio::test] +async fn run_operator_setup_rejects_serve_weak_webui_access_token_before_provider_write() { + let llm_config = Arc::new(SetupRecordingLlmConfigService::default()); + let services = services_with_setup_llm_config(llm_config.clone()); + + let err = services + .run_operator_setup( + caller(), + RebornOperatorSetupRequest { + provider_id: Some("openai".to_string()), + adapter: Some("open_ai_completions".to_string()), + webui_access_token: Some(SecretString::from("x".repeat(16))), + ..Default::default() + }, + ) + .await + .expect_err("16-byte WebUI token is rejected"); + + assert_setup_validation( + err, + "webui_access_token", + WebUiInboundValidationCode::InvalidValue, + ); + assert_eq!(llm_config.upsert_provider_count(), 0); + assert_eq!(llm_config.set_active_count(), 0); +} + #[tokio::test] async fn run_operator_setup_rejects_oversized_webui_access_token_before_provider_write() { let llm_config = Arc::new(SetupRecordingLlmConfigService::default()); From d908d595443a99ac5a9e595cbbe4a922982638bb Mon Sep 17 00:00:00 2001 From: Robert Yan Date: Tue, 23 Jun 2026 10:45:05 +0000 Subject: [PATCH 6/6] Fix operator setup token test fixtures --- .../tests/reborn_services_contract.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs b/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs index 4fc7fa94108..5b01a87c83a 100644 --- a/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs +++ b/crates/ironclaw_product_workflow/tests/reborn_services_contract.rs @@ -7993,7 +7993,7 @@ async fn run_operator_setup_rejects_unwired_host_mutations_before_provider_write adapter: Some("open_ai_completions".to_string()), profile_id: Some("production".to_string()), webui_access_token: Some(SecretString::from( - "webui-secret-token-value".to_string(), + "webui-secret-token-value-32-bytes".to_string(), )), ..Default::default() }, @@ -8045,7 +8045,7 @@ async fn run_operator_setup_rejects_token_only_host_mutation_before_provider_wri provider_id: Some("openai".to_string()), adapter: Some("open_ai_completions".to_string()), webui_access_token: Some(SecretString::from( - "webui-secret-token-value".to_string(), + "webui-secret-token-value-32-bytes".to_string(), )), ..Default::default() },