diff --git a/epiphany-core/src/admitted_model_direction_consideration.rs b/epiphany-core/src/admitted_model_direction_consideration.rs index 2445bb4b..7ba07c5b 100644 --- a/epiphany-core/src/admitted_model_direction_consideration.rs +++ b/epiphany-core/src/admitted_model_direction_consideration.rs @@ -9,6 +9,7 @@ pub const REQUEST_CONTRACT: &str = "epiphany.admitted_model_direction_considerat pub const RESULT_SCHEMA: &str = "epiphany.imagination.admitted_model_direction_consideration_result.v0"; pub const RESULT_CONTRACT: &str = "epiphany.admitted_model_direction_consideration_result.v0"; +pub const MAX_OPTION_DRAFTS: usize = 3; #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -252,6 +253,9 @@ pub fn validate_result( { bail!("model direction suggestion requires at least one option"); } + if result.option_drafts.len() > MAX_OPTION_DRAFTS { + bail!("model direction result exceeds bounded option fan-out"); + } Ok(()) } @@ -314,6 +318,20 @@ mod tests { } } + #[test] + fn result_refuses_unbounded_autonomous_proposal_fanout() { + let request = request(); + let mut result = result(&request); + result.disposition = AdmittedModelDirectionDisposition::Suggest; + result.option_drafts = (0..=MAX_OPTION_DRAFTS) + .map(|ordinal| crate::ImaginationOptionDraft { + title: format!("Option {ordinal}"), + summary: "Bounded option.".into(), + }) + .collect(); + assert!(validate_result(&request, &result).is_err()); + } + #[test] fn result_requires_exact_request_model_receipt_and_proposal_only_terminal_authority() { let request = request(); diff --git a/epiphany-core/src/agent_launch.rs b/epiphany-core/src/agent_launch.rs index 673787a0..b612852e 100644 --- a/epiphany-core/src/agent_launch.rs +++ b/epiphany-core/src/agent_launch.rs @@ -1083,7 +1083,7 @@ pub fn epiphany_admitted_model_direction_consideration_output_schema() -> serde_ "properties": { "disposition": {"type": "string", "enum": ["suggest", "hold", "no_fit"]}, "summary": {"type": "string", "minLength": 1}, - "option_drafts": {"type": "array", "items": {"type": "object", "required": ["title", "summary"], + "option_drafts": {"type": "array", "maxItems": crate::admitted_model_direction_consideration::MAX_OPTION_DRAFTS, "items": {"type": "object", "required": ["title", "summary"], "properties": {"title": {"type": "string", "minLength": 1}, "summary": {"type": "string", "minLength": 1}}, "additionalProperties": false}}, "uncertainties": {"type": "array", "items": {"type": "string"}}, @@ -1690,6 +1690,16 @@ fn epiphany_active_graph_node_ids(state: Option<&EpiphanyThreadState>) -> Vec