1use serde::{Deserialize, Serialize};
4
5pub const A2A_PROTOCOL_NAME: &str = "oris.a2a";
6pub const A2A_PROTOCOL_VERSION: &str = "0.1.0-experimental";
7pub const A2A_PROTOCOL_VERSION_V1: &str = "1.0.0";
8pub const A2A_SUPPORTED_PROTOCOL_VERSIONS: [&str; 2] =
9 [A2A_PROTOCOL_VERSION_V1, A2A_PROTOCOL_VERSION];
10
11#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
12pub struct A2aProtocol {
13 pub name: String,
14 pub version: String,
15}
16
17impl A2aProtocol {
18 pub fn current() -> Self {
19 Self {
20 name: A2A_PROTOCOL_NAME.to_string(),
21 version: A2A_PROTOCOL_VERSION.to_string(),
22 }
23 }
24}
25
26#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
27pub enum A2aCapability {
28 Coordination,
29 MutationProposal,
30 ReplayFeedback,
31 SupervisedDevloop,
32 EvolutionPublish,
33 EvolutionFetch,
34 EvolutionRevoke,
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
38pub struct A2aHandshakeRequest {
39 pub agent_id: String,
40 pub role: AgentRole,
41 pub capability_level: AgentCapabilityLevel,
42 pub supported_protocols: Vec<A2aProtocol>,
43 pub advertised_capabilities: Vec<A2aCapability>,
44}
45
46impl A2aHandshakeRequest {
47 pub fn supports_protocol_version(&self, version: &str) -> bool {
48 self.supported_protocols
49 .iter()
50 .any(|protocol| protocol.name == A2A_PROTOCOL_NAME && protocol.version == version)
51 }
52
53 pub fn supports_current_protocol(&self) -> bool {
54 self.supports_protocol_version(A2A_PROTOCOL_VERSION)
55 }
56
57 pub fn negotiate_supported_protocol(&self) -> Option<A2aProtocol> {
58 for version in A2A_SUPPORTED_PROTOCOL_VERSIONS {
59 if self.supports_protocol_version(version) {
60 return Some(A2aProtocol {
61 name: A2A_PROTOCOL_NAME.to_string(),
62 version: version.to_string(),
63 });
64 }
65 }
66 None
67 }
68}
69
70#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
71pub struct A2aHandshakeResponse {
72 pub accepted: bool,
73 pub negotiated_protocol: Option<A2aProtocol>,
74 pub enabled_capabilities: Vec<A2aCapability>,
75 pub message: Option<String>,
76 pub error: Option<A2aErrorEnvelope>,
77}
78
79impl A2aHandshakeResponse {
80 pub fn accept(enabled_capabilities: Vec<A2aCapability>) -> Self {
81 Self {
82 accepted: true,
83 negotiated_protocol: Some(A2aProtocol::current()),
84 enabled_capabilities,
85 message: Some("handshake accepted".to_string()),
86 error: None,
87 }
88 }
89
90 pub fn reject(code: A2aErrorCode, message: impl Into<String>, details: Option<String>) -> Self {
91 Self {
92 accepted: false,
93 negotiated_protocol: None,
94 enabled_capabilities: Vec::new(),
95 message: Some("handshake rejected".to_string()),
96 error: Some(A2aErrorEnvelope {
97 code,
98 message: message.into(),
99 retriable: true,
100 details,
101 }),
102 }
103 }
104}
105
106#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
107pub enum A2aTaskLifecycleState {
108 Queued,
109 Running,
110 Succeeded,
111 Failed,
112 Cancelled,
113}
114
115#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
116pub struct A2aTaskLifecycleEvent {
117 pub task_id: String,
118 pub state: A2aTaskLifecycleState,
119 pub summary: String,
120 pub updated_at_ms: u64,
121 pub error: Option<A2aErrorEnvelope>,
122}
123
124pub const A2A_TASK_SESSION_PROTOCOL_VERSION: &str = A2A_PROTOCOL_VERSION;
125
126#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
127pub enum A2aTaskSessionState {
128 Started,
129 Dispatched,
130 InProgress,
131 Completed,
132 Failed,
133 Cancelled,
134}
135
136#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
137pub struct A2aTaskSessionStartRequest {
138 pub sender_id: String,
139 pub protocol_version: String,
140 pub task_id: String,
141 pub task_summary: String,
142}
143
144#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
145pub struct A2aTaskSessionDispatchRequest {
146 pub sender_id: String,
147 pub protocol_version: String,
148 pub dispatch_id: String,
149 pub summary: String,
150}
151
152#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
153pub struct A2aTaskSessionProgressRequest {
154 pub sender_id: String,
155 pub protocol_version: String,
156 pub progress_pct: u8,
157 pub summary: String,
158 pub retryable: bool,
159 pub retry_after_ms: Option<u64>,
160}
161
162#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
163pub struct A2aTaskSessionCompletionRequest {
164 pub sender_id: String,
165 pub protocol_version: String,
166 pub terminal_state: A2aTaskLifecycleState,
167 pub summary: String,
168 pub retryable: bool,
169 pub retry_after_ms: Option<u64>,
170 pub failure_code: Option<A2aErrorCode>,
171 pub failure_details: Option<String>,
172 pub used_capsule: bool,
173 pub capsule_id: Option<String>,
174 pub reasoning_steps_avoided: u64,
175 pub fallback_reason: Option<String>,
176 pub reason_code: Option<ReplayFallbackReasonCode>,
177 pub repair_hint: Option<String>,
178 pub next_action: Option<ReplayFallbackNextAction>,
179 pub confidence: Option<u8>,
180 pub task_class_id: String,
181 pub task_label: String,
182}
183
184#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
185pub struct A2aTaskSessionProgressItem {
186 pub progress_pct: u8,
187 pub summary: String,
188 pub retryable: bool,
189 pub retry_after_ms: Option<u64>,
190 pub updated_at_ms: u64,
191}
192
193#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
194pub struct A2aTaskSessionAck {
195 pub session_id: String,
196 pub task_id: String,
197 pub state: A2aTaskSessionState,
198 pub summary: String,
199 pub retryable: bool,
200 pub retry_after_ms: Option<u64>,
201 pub updated_at_ms: u64,
202}
203
204#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
205pub struct A2aTaskSessionResult {
206 pub terminal_state: A2aTaskLifecycleState,
207 pub summary: String,
208 pub retryable: bool,
209 pub retry_after_ms: Option<u64>,
210 pub failure_code: Option<A2aErrorCode>,
211 pub failure_details: Option<String>,
212 pub replay_feedback: ReplayFeedback,
213}
214
215#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
216pub struct A2aTaskSessionCompletionResponse {
217 pub ack: A2aTaskSessionAck,
218 pub result: A2aTaskSessionResult,
219}
220
221#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
222pub struct A2aTaskSessionSnapshot {
223 pub session_id: String,
224 pub sender_id: String,
225 pub task_id: String,
226 pub protocol_version: String,
227 pub state: A2aTaskSessionState,
228 pub created_at_ms: u64,
229 pub updated_at_ms: u64,
230 pub dispatch_ids: Vec<String>,
231 pub progress: Vec<A2aTaskSessionProgressItem>,
232 pub result: Option<A2aTaskSessionResult>,
233}
234
235#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
236pub enum A2aErrorCode {
237 UnsupportedProtocol,
238 UnsupportedCapability,
239 ValidationFailed,
240 AuthorizationDenied,
241 Timeout,
242 Internal,
243}
244
245#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
246pub struct A2aErrorEnvelope {
247 pub code: A2aErrorCode,
248 pub message: String,
249 pub retriable: bool,
250 pub details: Option<String>,
251}
252
253#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
254pub enum AgentCapabilityLevel {
255 A0,
256 A1,
257 A2,
258 A3,
259 A4,
260}
261
262#[derive(Clone, Debug, Serialize, Deserialize)]
263pub enum ProposalTarget {
264 WorkspaceRoot,
265 Paths(Vec<String>),
266}
267
268#[derive(Clone, Debug, Serialize, Deserialize)]
269pub struct AgentTask {
270 pub id: String,
271 pub description: String,
272}
273
274#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
275pub enum AgentRole {
276 Planner,
277 Coder,
278 Repair,
279 Optimizer,
280}
281
282#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
283pub enum CoordinationPrimitive {
284 Sequential,
285 Parallel,
286 Conditional,
287}
288
289#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
290pub struct CoordinationTask {
291 pub id: String,
292 pub role: AgentRole,
293 pub description: String,
294 pub depends_on: Vec<String>,
295}
296
297#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
298pub struct CoordinationMessage {
299 pub from_role: AgentRole,
300 pub to_role: AgentRole,
301 pub task_id: String,
302 pub content: String,
303}
304
305#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
306pub struct CoordinationPlan {
307 pub root_goal: String,
308 pub primitive: CoordinationPrimitive,
309 pub tasks: Vec<CoordinationTask>,
310 pub timeout_ms: u64,
311 pub max_retries: u32,
312}
313
314#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
315pub struct CoordinationResult {
316 pub completed_tasks: Vec<String>,
317 pub failed_tasks: Vec<String>,
318 pub messages: Vec<CoordinationMessage>,
319 pub summary: String,
320}
321
322#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
323pub struct MutationProposal {
324 pub intent: String,
325 pub files: Vec<String>,
326 pub expected_effect: String,
327}
328
329#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
330#[serde(rename_all = "snake_case")]
331pub enum MutationProposalContractReasonCode {
332 Accepted,
333 CandidateRejected,
334 MissingTargetFiles,
335 OutOfBoundsPath,
336 UnsupportedTaskClass,
337 ValidationBudgetExceeded,
338 ExpectedEvidenceMissing,
339 UnknownFailClosed,
340}
341
342#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
343#[serde(rename_all = "snake_case")]
344pub enum MutationProposalEvidence {
345 HumanApproval,
346 BoundedScope,
347 ValidationPass,
348 ExecutionAudit,
349}
350
351#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
352pub struct MutationProposalValidationBudget {
353 pub max_diff_bytes: usize,
354 pub max_changed_lines: usize,
355 pub validation_timeout_ms: u64,
356}
357
358#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
359pub struct ExecutionFeedback {
360 pub accepted: bool,
361 pub asset_state: Option<String>,
362 pub summary: String,
363}
364
365#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
366pub enum ReplayPlannerDirective {
367 SkipPlanner,
368 PlanFallback,
369}
370
371#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
372#[serde(rename_all = "snake_case")]
373pub enum ReplayFallbackReasonCode {
374 NoCandidateAfterSelect,
375 ScoreBelowThreshold,
376 CandidateHasNoCapsule,
377 MutationPayloadMissing,
378 PatchApplyFailed,
379 ValidationFailed,
380 UnmappedFallbackReason,
381}
382
383#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
384#[serde(rename_all = "snake_case")]
385pub enum ReplayFallbackNextAction {
386 PlanFromScratch,
387 ValidateSignalsThenPlan,
388 RebuildCapsule,
389 RegenerateMutationPayload,
390 RebasePatchAndRetry,
391 RepairAndRevalidate,
392 EscalateFailClosed,
393}
394
395#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
396pub struct ReplayFallbackContract {
397 pub reason_code: ReplayFallbackReasonCode,
398 pub fallback_reason: String,
399 pub repair_hint: String,
400 pub next_action: ReplayFallbackNextAction,
401 pub confidence: u8,
403}
404
405pub fn infer_replay_fallback_reason_code(reason: &str) -> Option<ReplayFallbackReasonCode> {
406 let normalized = reason.trim().to_ascii_lowercase();
407 if normalized.is_empty() {
408 return None;
409 }
410 if normalized == "no_candidate_after_select" || normalized.contains("no matching gene") {
411 return Some(ReplayFallbackReasonCode::NoCandidateAfterSelect);
412 }
413 if normalized == "score_below_threshold" || normalized.contains("below replay threshold") {
414 return Some(ReplayFallbackReasonCode::ScoreBelowThreshold);
415 }
416 if normalized == "candidate_has_no_capsule" || normalized.contains("has no capsule") {
417 return Some(ReplayFallbackReasonCode::CandidateHasNoCapsule);
418 }
419 if normalized == "mutation_payload_missing" || normalized.contains("payload missing") {
420 return Some(ReplayFallbackReasonCode::MutationPayloadMissing);
421 }
422 if normalized == "patch_apply_failed" || normalized.contains("patch apply failed") {
423 return Some(ReplayFallbackReasonCode::PatchApplyFailed);
424 }
425 if normalized == "validation_failed" || normalized.contains("validation failed") {
426 return Some(ReplayFallbackReasonCode::ValidationFailed);
427 }
428 None
429}
430
431pub fn normalize_replay_fallback_contract(
432 planner_directive: &ReplayPlannerDirective,
433 fallback_reason: Option<&str>,
434 reason_code: Option<ReplayFallbackReasonCode>,
435 repair_hint: Option<&str>,
436 next_action: Option<ReplayFallbackNextAction>,
437 confidence: Option<u8>,
438) -> Option<ReplayFallbackContract> {
439 if !matches!(planner_directive, ReplayPlannerDirective::PlanFallback) {
440 return None;
441 }
442
443 let normalized_reason = normalize_optional_text(fallback_reason);
444 let normalized_repair_hint = normalize_optional_text(repair_hint);
445 let mut resolved_reason_code = reason_code
446 .or_else(|| {
447 normalized_reason
448 .as_deref()
449 .and_then(infer_replay_fallback_reason_code)
450 })
451 .unwrap_or(ReplayFallbackReasonCode::UnmappedFallbackReason);
452 let mut defaults = replay_fallback_defaults(&resolved_reason_code);
453
454 let mut force_fail_closed = false;
455 if let Some(provided_action) = next_action {
456 if provided_action != defaults.next_action {
457 resolved_reason_code = ReplayFallbackReasonCode::UnmappedFallbackReason;
458 defaults = replay_fallback_defaults(&resolved_reason_code);
459 force_fail_closed = true;
460 }
461 }
462
463 Some(ReplayFallbackContract {
464 reason_code: resolved_reason_code,
465 fallback_reason: normalized_reason.unwrap_or_else(|| defaults.fallback_reason.to_string()),
466 repair_hint: normalized_repair_hint.unwrap_or_else(|| defaults.repair_hint.to_string()),
467 next_action: if force_fail_closed {
468 defaults.next_action
469 } else {
470 next_action.unwrap_or(defaults.next_action)
471 },
472 confidence: if force_fail_closed {
473 defaults.confidence
474 } else {
475 confidence.unwrap_or(defaults.confidence).min(100)
476 },
477 })
478}
479
480#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
481pub struct ReplayFeedback {
482 pub used_capsule: bool,
483 pub capsule_id: Option<String>,
484 pub planner_directive: ReplayPlannerDirective,
485 pub reasoning_steps_avoided: u64,
486 pub fallback_reason: Option<String>,
487 pub reason_code: Option<ReplayFallbackReasonCode>,
488 pub repair_hint: Option<String>,
489 pub next_action: Option<ReplayFallbackNextAction>,
490 pub confidence: Option<u8>,
491 pub task_class_id: String,
492 pub task_label: String,
493 pub summary: String,
494}
495
496#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
497#[serde(rename_all = "snake_case")]
498pub enum MutationNeededFailureReasonCode {
499 PolicyDenied,
500 ValidationFailed,
501 UnsafePatch,
502 Timeout,
503 MutationPayloadMissing,
504 UnknownFailClosed,
505}
506
507#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
508#[serde(rename_all = "snake_case")]
509pub enum MutationNeededRecoveryAction {
510 NarrowScopeAndRetry,
511 RepairAndRevalidate,
512 ProduceSafePatch,
513 ReduceExecutionBudget,
514 RegenerateMutationPayload,
515 EscalateFailClosed,
516}
517
518#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
519pub struct MutationNeededFailureContract {
520 pub reason_code: MutationNeededFailureReasonCode,
521 pub failure_reason: String,
522 pub recovery_hint: String,
523 pub recovery_action: MutationNeededRecoveryAction,
524 pub fail_closed: bool,
525}
526
527pub fn infer_mutation_needed_failure_reason_code(
528 reason: &str,
529) -> Option<MutationNeededFailureReasonCode> {
530 let normalized = reason.trim().to_ascii_lowercase();
531 if normalized.is_empty() {
532 return None;
533 }
534 if normalized.contains("mutation payload missing") || normalized == "mutation_payload_missing" {
535 return Some(MutationNeededFailureReasonCode::MutationPayloadMissing);
536 }
537 if normalized.contains("command timed out") || normalized.contains(" timeout") {
538 return Some(MutationNeededFailureReasonCode::Timeout);
539 }
540 if normalized.contains("patch rejected")
541 || normalized.contains("patch apply failed")
542 || normalized.contains("target violation")
543 || normalized.contains("unsafe patch")
544 {
545 return Some(MutationNeededFailureReasonCode::UnsafePatch);
546 }
547 if normalized.contains("validation failed") {
548 return Some(MutationNeededFailureReasonCode::ValidationFailed);
549 }
550 if normalized.contains("command denied by policy")
551 || normalized.contains("rejected task")
552 || normalized.contains("unsupported task outside the bounded scope")
553 || normalized.contains("budget exceeds bounded policy")
554 {
555 return Some(MutationNeededFailureReasonCode::PolicyDenied);
556 }
557 None
558}
559
560pub fn normalize_mutation_needed_failure_contract(
561 failure_reason: Option<&str>,
562 reason_code: Option<MutationNeededFailureReasonCode>,
563) -> MutationNeededFailureContract {
564 let normalized_reason = normalize_optional_text(failure_reason);
565 let resolved_reason_code = reason_code
566 .or_else(|| {
567 normalized_reason
568 .as_deref()
569 .and_then(infer_mutation_needed_failure_reason_code)
570 })
571 .unwrap_or(MutationNeededFailureReasonCode::UnknownFailClosed);
572 let defaults = mutation_needed_failure_defaults(&resolved_reason_code);
573
574 MutationNeededFailureContract {
575 reason_code: resolved_reason_code,
576 failure_reason: normalized_reason.unwrap_or_else(|| defaults.failure_reason.to_string()),
577 recovery_hint: defaults.recovery_hint.to_string(),
578 recovery_action: defaults.recovery_action,
579 fail_closed: true,
580 }
581}
582
583fn normalize_optional_text(value: Option<&str>) -> Option<String> {
584 let trimmed = value?.trim();
585 if trimmed.is_empty() {
586 None
587 } else {
588 Some(trimmed.to_string())
589 }
590}
591
592#[derive(Clone, Copy)]
593struct ReplayFallbackDefaults {
594 fallback_reason: &'static str,
595 repair_hint: &'static str,
596 next_action: ReplayFallbackNextAction,
597 confidence: u8,
598}
599
600fn replay_fallback_defaults(reason_code: &ReplayFallbackReasonCode) -> ReplayFallbackDefaults {
601 match reason_code {
602 ReplayFallbackReasonCode::NoCandidateAfterSelect => ReplayFallbackDefaults {
603 fallback_reason: "no matching gene",
604 repair_hint:
605 "No reusable capsule matched deterministic signals; run planner for a minimal patch.",
606 next_action: ReplayFallbackNextAction::PlanFromScratch,
607 confidence: 92,
608 },
609 ReplayFallbackReasonCode::ScoreBelowThreshold => ReplayFallbackDefaults {
610 fallback_reason: "candidate score below replay threshold",
611 repair_hint:
612 "Best replay candidate is below threshold; validate task signals and re-plan.",
613 next_action: ReplayFallbackNextAction::ValidateSignalsThenPlan,
614 confidence: 86,
615 },
616 ReplayFallbackReasonCode::CandidateHasNoCapsule => ReplayFallbackDefaults {
617 fallback_reason: "candidate gene has no capsule",
618 repair_hint: "Matched gene has no executable capsule; rebuild capsule from planner output.",
619 next_action: ReplayFallbackNextAction::RebuildCapsule,
620 confidence: 80,
621 },
622 ReplayFallbackReasonCode::MutationPayloadMissing => ReplayFallbackDefaults {
623 fallback_reason: "mutation payload missing from store",
624 repair_hint:
625 "Mutation payload is missing; regenerate and persist a minimal mutation payload.",
626 next_action: ReplayFallbackNextAction::RegenerateMutationPayload,
627 confidence: 76,
628 },
629 ReplayFallbackReasonCode::PatchApplyFailed => ReplayFallbackDefaults {
630 fallback_reason: "replay patch apply failed",
631 repair_hint: "Replay patch cannot be applied cleanly; rebase patch and retry planning.",
632 next_action: ReplayFallbackNextAction::RebasePatchAndRetry,
633 confidence: 68,
634 },
635 ReplayFallbackReasonCode::ValidationFailed => ReplayFallbackDefaults {
636 fallback_reason: "replay validation failed",
637 repair_hint: "Replay validation failed; produce a repair mutation and re-run validation.",
638 next_action: ReplayFallbackNextAction::RepairAndRevalidate,
639 confidence: 64,
640 },
641 ReplayFallbackReasonCode::UnmappedFallbackReason => ReplayFallbackDefaults {
642 fallback_reason: "unmapped replay fallback reason",
643 repair_hint:
644 "Fallback reason is unmapped; fail closed and require explicit planner intervention.",
645 next_action: ReplayFallbackNextAction::EscalateFailClosed,
646 confidence: 0,
647 },
648 }
649}
650
651#[derive(Clone, Copy)]
652struct MutationNeededFailureDefaults {
653 failure_reason: &'static str,
654 recovery_hint: &'static str,
655 recovery_action: MutationNeededRecoveryAction,
656}
657
658fn mutation_needed_failure_defaults(
659 reason_code: &MutationNeededFailureReasonCode,
660) -> MutationNeededFailureDefaults {
661 match reason_code {
662 MutationNeededFailureReasonCode::PolicyDenied => MutationNeededFailureDefaults {
663 failure_reason: "mutation needed denied by bounded execution policy",
664 recovery_hint:
665 "Narrow changed scope to the approved docs boundary and re-run with explicit approval.",
666 recovery_action: MutationNeededRecoveryAction::NarrowScopeAndRetry,
667 },
668 MutationNeededFailureReasonCode::ValidationFailed => MutationNeededFailureDefaults {
669 failure_reason: "mutation needed validation failed",
670 recovery_hint:
671 "Repair mutation and re-run validation to produce a deterministic pass before capture.",
672 recovery_action: MutationNeededRecoveryAction::RepairAndRevalidate,
673 },
674 MutationNeededFailureReasonCode::UnsafePatch => MutationNeededFailureDefaults {
675 failure_reason: "mutation needed rejected unsafe patch",
676 recovery_hint:
677 "Generate a safer minimal diff confined to approved paths and verify patch applicability.",
678 recovery_action: MutationNeededRecoveryAction::ProduceSafePatch,
679 },
680 MutationNeededFailureReasonCode::Timeout => MutationNeededFailureDefaults {
681 failure_reason: "mutation needed execution timed out",
682 recovery_hint:
683 "Reduce execution budget or split the mutation into smaller steps before retrying.",
684 recovery_action: MutationNeededRecoveryAction::ReduceExecutionBudget,
685 },
686 MutationNeededFailureReasonCode::MutationPayloadMissing => MutationNeededFailureDefaults {
687 failure_reason: "mutation payload missing from store",
688 recovery_hint: "Regenerate and persist mutation payload before retrying mutation-needed.",
689 recovery_action: MutationNeededRecoveryAction::RegenerateMutationPayload,
690 },
691 MutationNeededFailureReasonCode::UnknownFailClosed => MutationNeededFailureDefaults {
692 failure_reason: "mutation needed failed with unmapped reason",
693 recovery_hint:
694 "Unknown failure class; fail closed and require explicit maintainer triage before retry.",
695 recovery_action: MutationNeededRecoveryAction::EscalateFailClosed,
696 },
697 }
698}
699
700#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
701pub enum BoundedTaskClass {
702 DocsSingleFile,
703 DocsMultiFile,
704 CargoDepUpgrade,
707 LintFix,
710}
711
712#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
713pub struct MutationProposalScope {
714 pub task_class: BoundedTaskClass,
715 pub target_files: Vec<String>,
716}
717
718#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
719pub struct SelfEvolutionMutationProposalContract {
720 pub mutation_proposal: MutationProposal,
721 #[serde(default, skip_serializing_if = "Option::is_none")]
722 pub proposal_scope: Option<MutationProposalScope>,
723 pub validation_budget: MutationProposalValidationBudget,
724 pub approval_required: bool,
725 pub expected_evidence: Vec<MutationProposalEvidence>,
726 pub summary: String,
727 #[serde(default, skip_serializing_if = "Option::is_none")]
728 pub failure_reason: Option<String>,
729 #[serde(default, skip_serializing_if = "Option::is_none")]
730 pub recovery_hint: Option<String>,
731 pub reason_code: MutationProposalContractReasonCode,
732 pub fail_closed: bool,
733}
734
735#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
736pub struct SelfEvolutionCandidateIntakeRequest {
737 pub issue_number: u64,
738 pub title: String,
739 pub body: String,
740 #[serde(default)]
741 pub labels: Vec<String>,
742 pub state: String,
743 #[serde(default)]
744 pub candidate_hint_paths: Vec<String>,
745}
746
747#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
749#[serde(rename_all = "snake_case")]
750pub enum AutonomousCandidateSource {
751 CiFailure,
752 TestRegression,
753 CompileRegression,
754 LintRegression,
755 RuntimeIncident,
756}
757
758#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
760#[serde(rename_all = "snake_case")]
761pub enum AutonomousIntakeReasonCode {
762 Accepted,
763 UnsupportedSignalClass,
764 AmbiguousSignal,
765 DuplicateCandidate,
766 UnknownFailClosed,
767}
768
769#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
772pub struct DiscoveredCandidate {
773 pub dedupe_key: String,
775 pub candidate_source: AutonomousCandidateSource,
777 pub candidate_class: Option<BoundedTaskClass>,
779 pub signals: Vec<String>,
781 pub accepted: bool,
783 pub reason_code: AutonomousIntakeReasonCode,
785 pub summary: String,
787 #[serde(default, skip_serializing_if = "Option::is_none")]
788 pub failure_reason: Option<String>,
789 #[serde(default, skip_serializing_if = "Option::is_none")]
790 pub recovery_hint: Option<String>,
791 pub fail_closed: bool,
793}
794
795#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
797pub struct AutonomousIntakeInput {
798 pub source_id: String,
800 pub candidate_source: AutonomousCandidateSource,
802 pub raw_signals: Vec<String>,
804}
805
806#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
809pub struct AutonomousIntakeOutput {
810 pub candidates: Vec<DiscoveredCandidate>,
811 pub accepted_count: usize,
812 pub denied_count: usize,
813}
814
815#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
819#[serde(rename_all = "snake_case")]
820pub enum AutonomousRiskTier {
821 Low,
823 Medium,
825 High,
827}
828
829#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
831#[serde(rename_all = "snake_case")]
832pub enum AutonomousPlanReasonCode {
833 Approved,
834 DeniedHighRisk,
835 DeniedLowFeasibility,
836 DeniedUnsupportedClass,
837 DeniedNoEvidence,
838 UnknownFailClosed,
839}
840
841#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
843pub struct AutonomousDenialCondition {
844 pub reason_code: AutonomousPlanReasonCode,
845 pub description: String,
846 pub recovery_hint: String,
847}
848
849#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
851pub struct AutonomousTaskPlan {
852 pub plan_id: String,
854 pub dedupe_key: String,
856 pub task_class: Option<BoundedTaskClass>,
858 pub risk_tier: AutonomousRiskTier,
860 pub feasibility_score: u8,
862 pub validation_budget: u8,
864 pub expected_evidence: Vec<String>,
866 pub approved: bool,
868 pub reason_code: AutonomousPlanReasonCode,
870 pub summary: String,
872 #[serde(default, skip_serializing_if = "Option::is_none")]
874 pub denial_condition: Option<AutonomousDenialCondition>,
875 pub fail_closed: bool,
877}
878
879pub fn approve_autonomous_task_plan(
880 plan_id: impl Into<String>,
881 dedupe_key: impl Into<String>,
882 task_class: BoundedTaskClass,
883 risk_tier: AutonomousRiskTier,
884 feasibility_score: u8,
885 validation_budget: u8,
886 expected_evidence: Vec<String>,
887 summary: Option<&str>,
888) -> AutonomousTaskPlan {
889 let summary = normalize_optional_text(summary).unwrap_or_else(|| {
890 format!("autonomous task plan approved for {task_class:?} at {risk_tier:?} risk")
891 });
892 AutonomousTaskPlan {
893 plan_id: plan_id.into(),
894 dedupe_key: dedupe_key.into(),
895 task_class: Some(task_class),
896 risk_tier,
897 feasibility_score,
898 validation_budget,
899 expected_evidence,
900 approved: true,
901 reason_code: AutonomousPlanReasonCode::Approved,
902 summary,
903 denial_condition: None,
904 fail_closed: false,
905 }
906}
907
908pub fn deny_autonomous_task_plan(
909 plan_id: impl Into<String>,
910 dedupe_key: impl Into<String>,
911 risk_tier: AutonomousRiskTier,
912 reason_code: AutonomousPlanReasonCode,
913) -> AutonomousTaskPlan {
914 let (description, recovery_hint) = match reason_code {
915 AutonomousPlanReasonCode::DeniedHighRisk => (
916 "task plan denied because risk tier is too high for autonomous execution",
917 "reduce blast radius by scoping the change to a single bounded file before retrying",
918 ),
919 AutonomousPlanReasonCode::DeniedLowFeasibility => (
920 "task plan denied because feasibility score is below the policy threshold",
921 "provide stronger evidence or narrow the task scope before retrying",
922 ),
923 AutonomousPlanReasonCode::DeniedUnsupportedClass => (
924 "task plan denied because task class is not supported for autonomous planning",
925 "route this task class through the supervised planning path instead",
926 ),
927 AutonomousPlanReasonCode::DeniedNoEvidence => (
928 "task plan denied because no evidence was available to assess feasibility",
929 "ensure signals and candidate class are populated before planning",
930 ),
931 AutonomousPlanReasonCode::UnknownFailClosed => (
932 "task plan failed with an unmapped reason; fail closed",
933 "require explicit maintainer triage before retry",
934 ),
935 AutonomousPlanReasonCode::Approved => (
936 "unexpected approved reason on deny path",
937 "use approve_autonomous_task_plan for approved outcomes",
938 ),
939 };
940 let summary = format!("autonomous task plan denied [{reason_code:?}]: {description}");
941 AutonomousTaskPlan {
942 plan_id: plan_id.into(),
943 dedupe_key: dedupe_key.into(),
944 task_class: None,
945 risk_tier,
946 feasibility_score: 0,
947 validation_budget: 0,
948 expected_evidence: Vec::new(),
949 approved: false,
950 reason_code,
951 summary,
952 denial_condition: Some(AutonomousDenialCondition {
953 reason_code,
954 description: description.to_string(),
955 recovery_hint: recovery_hint.to_string(),
956 }),
957 fail_closed: true,
958 }
959}
960
961#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
965#[serde(rename_all = "snake_case")]
966pub enum AutonomousApprovalMode {
967 AutoApproved,
969 RequiresHumanReview,
971}
972
973#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
975#[serde(rename_all = "snake_case")]
976pub enum AutonomousProposalReasonCode {
977 Proposed,
978 DeniedPlanNotApproved,
979 DeniedNoTargetScope,
980 DeniedWeakEvidence,
981 DeniedOutOfBounds,
982 UnknownFailClosed,
983}
984
985#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
987pub struct AutonomousProposalScope {
988 pub target_paths: Vec<String>,
990 pub scope_rationale: String,
992 pub max_files: u8,
994}
995
996#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
998pub struct AutonomousMutationProposal {
999 pub proposal_id: String,
1001 pub plan_id: String,
1003 pub dedupe_key: String,
1005 pub scope: Option<AutonomousProposalScope>,
1007 pub expected_evidence: Vec<String>,
1009 pub rollback_conditions: Vec<String>,
1011 pub approval_mode: AutonomousApprovalMode,
1013 pub proposed: bool,
1015 pub reason_code: AutonomousProposalReasonCode,
1017 pub summary: String,
1019 #[serde(default, skip_serializing_if = "Option::is_none")]
1021 pub denial_condition: Option<AutonomousDenialCondition>,
1022 pub fail_closed: bool,
1024}
1025
1026pub fn approve_autonomous_mutation_proposal(
1028 proposal_id: impl Into<String>,
1029 plan_id: impl Into<String>,
1030 dedupe_key: impl Into<String>,
1031 scope: AutonomousProposalScope,
1032 expected_evidence: Vec<String>,
1033 rollback_conditions: Vec<String>,
1034 approval_mode: AutonomousApprovalMode,
1035 summary: Option<&str>,
1036) -> AutonomousMutationProposal {
1037 let dedupe_key = dedupe_key.into();
1038 let summary = summary
1039 .and_then(|s| {
1040 if s.trim().is_empty() {
1041 None
1042 } else {
1043 Some(s.to_string())
1044 }
1045 })
1046 .unwrap_or_else(|| format!("autonomous mutation proposal approved for {dedupe_key}"));
1047 AutonomousMutationProposal {
1048 proposal_id: proposal_id.into(),
1049 plan_id: plan_id.into(),
1050 dedupe_key,
1051 scope: Some(scope),
1052 expected_evidence,
1053 rollback_conditions,
1054 approval_mode,
1055 proposed: true,
1056 reason_code: AutonomousProposalReasonCode::Proposed,
1057 summary,
1058 denial_condition: None,
1059 fail_closed: false,
1060 }
1061}
1062
1063pub fn deny_autonomous_mutation_proposal(
1065 proposal_id: impl Into<String>,
1066 plan_id: impl Into<String>,
1067 dedupe_key: impl Into<String>,
1068 reason_code: AutonomousProposalReasonCode,
1069) -> AutonomousMutationProposal {
1070 let (description, recovery_hint) = match reason_code {
1071 AutonomousProposalReasonCode::DeniedPlanNotApproved => (
1072 "source plan was not approved; cannot generate proposal",
1073 "ensure the autonomous task plan is approved before proposing a mutation",
1074 ),
1075 AutonomousProposalReasonCode::DeniedNoTargetScope => (
1076 "no bounded target scope could be determined for this task class",
1077 "verify that the task class maps to a known set of target paths",
1078 ),
1079 AutonomousProposalReasonCode::DeniedWeakEvidence => (
1080 "the expected evidence set is empty or insufficient",
1081 "strengthen evidence requirements before reattempting proposal",
1082 ),
1083 AutonomousProposalReasonCode::DeniedOutOfBounds => (
1084 "proposal would mutate files outside of bounded policy scope",
1085 "restrict targets to explicitly allowed paths and retry",
1086 ),
1087 AutonomousProposalReasonCode::UnknownFailClosed => (
1088 "proposal generation failed with an unmapped reason; fail closed",
1089 "require explicit maintainer triage before retry",
1090 ),
1091 AutonomousProposalReasonCode::Proposed => (
1092 "unexpected proposed reason on deny path",
1093 "use approve_autonomous_mutation_proposal for proposed outcomes",
1094 ),
1095 };
1096 let dedupe_key = dedupe_key.into();
1097 let summary = format!("autonomous mutation proposal denied [{reason_code:?}]: {description}");
1098 AutonomousMutationProposal {
1099 proposal_id: proposal_id.into(),
1100 plan_id: plan_id.into(),
1101 dedupe_key,
1102 scope: None,
1103 expected_evidence: Vec::new(),
1104 rollback_conditions: Vec::new(),
1105 approval_mode: AutonomousApprovalMode::RequiresHumanReview,
1106 proposed: false,
1107 reason_code,
1108 summary,
1109 denial_condition: Some(AutonomousDenialCondition {
1110 reason_code: AutonomousPlanReasonCode::UnknownFailClosed,
1111 description: description.to_string(),
1112 recovery_hint: recovery_hint.to_string(),
1113 }),
1114 fail_closed: true,
1115 }
1116}
1117
1118#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1128#[serde(rename_all = "snake_case")]
1129pub enum TaskEquivalenceClass {
1130 DocumentationEdit,
1133 StaticAnalysisFix,
1135 DependencyManifestUpdate,
1137 Unclassified,
1139}
1140
1141#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1144pub struct EquivalenceExplanation {
1145 pub task_equivalence_class: TaskEquivalenceClass,
1147 pub rationale: String,
1149 pub matching_features: Vec<String>,
1151 pub replay_match_confidence: u8,
1153}
1154
1155#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1157#[serde(rename_all = "snake_case")]
1158pub enum SemanticReplayReasonCode {
1159 EquivalenceMatchApproved,
1161 LowConfidenceDenied,
1163 NoEquivalenceClassMatch,
1165 EquivalenceClassNotAllowed,
1168 UnknownFailClosed,
1170}
1171
1172#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1178pub struct SemanticReplayDecision {
1179 pub evaluation_id: String,
1181 pub task_id: String,
1183 pub replay_decision: bool,
1185 pub equivalence_explanation: Option<EquivalenceExplanation>,
1187 pub reason_code: SemanticReplayReasonCode,
1189 pub summary: String,
1191 pub fail_closed: bool,
1194}
1195
1196pub fn approve_semantic_replay(
1198 evaluation_id: impl Into<String>,
1199 task_id: impl Into<String>,
1200 explanation: EquivalenceExplanation,
1201) -> SemanticReplayDecision {
1202 let summary = format!(
1203 "semantic replay approved [equivalence_class={:?}, confidence={}]",
1204 explanation.task_equivalence_class, explanation.replay_match_confidence
1205 );
1206 SemanticReplayDecision {
1207 evaluation_id: evaluation_id.into(),
1208 task_id: task_id.into(),
1209 replay_decision: true,
1210 reason_code: SemanticReplayReasonCode::EquivalenceMatchApproved,
1211 equivalence_explanation: Some(explanation),
1212 summary,
1213 fail_closed: false,
1214 }
1215}
1216
1217pub fn deny_semantic_replay(
1219 evaluation_id: impl Into<String>,
1220 task_id: impl Into<String>,
1221 reason_code: SemanticReplayReasonCode,
1222 context: impl Into<String>,
1223) -> SemanticReplayDecision {
1224 let context: String = context.into();
1225 let summary = format!("semantic replay denied [{reason_code:?}]: {context}");
1226 SemanticReplayDecision {
1227 evaluation_id: evaluation_id.into(),
1228 task_id: task_id.into(),
1229 replay_decision: false,
1230 equivalence_explanation: None,
1231 reason_code,
1232 summary,
1233 fail_closed: true,
1234 }
1235}
1236
1237#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1243#[serde(rename_all = "snake_case")]
1244pub enum ConfidenceState {
1245 Active,
1247 Decaying,
1250 Revalidating,
1253 Demoted,
1256 Quarantined,
1259}
1260
1261#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1263#[serde(rename_all = "snake_case")]
1264pub enum RevalidationOutcome {
1265 Passed,
1267 Failed,
1269 Pending,
1271 ErrorFailClosed,
1273}
1274
1275#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1277#[serde(rename_all = "snake_case")]
1278pub enum ConfidenceDemotionReasonCode {
1279 ConfidenceDecayThreshold,
1281 RepeatedReplayFailure,
1283 MaxFailureCountExceeded,
1285 ExplicitRevocation,
1287 UnknownFailClosed,
1289}
1290
1291#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1293#[serde(rename_all = "snake_case")]
1294pub enum ReplayEligibility {
1295 Eligible,
1297 Ineligible,
1299}
1300
1301#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1303pub struct ConfidenceRevalidationResult {
1304 pub revalidation_id: String,
1306 pub asset_id: String,
1308 pub confidence_state: ConfidenceState,
1310 pub revalidation_result: RevalidationOutcome,
1312 pub replay_eligibility: ReplayEligibility,
1314 pub summary: String,
1316 pub fail_closed: bool,
1319}
1320
1321#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1323pub struct DemotionDecision {
1324 pub demotion_id: String,
1326 pub asset_id: String,
1328 pub prior_state: ConfidenceState,
1330 pub new_state: ConfidenceState,
1332 pub reason_code: ConfidenceDemotionReasonCode,
1334 pub replay_eligibility: ReplayEligibility,
1336 pub summary: String,
1338 pub quarantine_transition: bool,
1340 pub fail_closed: bool,
1342}
1343
1344pub fn pass_confidence_revalidation(
1346 revalidation_id: impl Into<String>,
1347 asset_id: impl Into<String>,
1348 prior_state: ConfidenceState,
1349) -> ConfidenceRevalidationResult {
1350 let asset_id: String = asset_id.into();
1351 let summary =
1352 format!("confidence revalidation passed for asset {asset_id}: restoring to Active");
1353 ConfidenceRevalidationResult {
1354 revalidation_id: revalidation_id.into(),
1355 asset_id,
1356 confidence_state: ConfidenceState::Active,
1357 revalidation_result: RevalidationOutcome::Passed,
1358 replay_eligibility: ReplayEligibility::Eligible,
1359 summary,
1360 fail_closed: false,
1361 }
1362}
1363
1364pub fn fail_confidence_revalidation(
1366 revalidation_id: impl Into<String>,
1367 asset_id: impl Into<String>,
1368 prior_state: ConfidenceState,
1369 outcome: RevalidationOutcome,
1370) -> ConfidenceRevalidationResult {
1371 let asset_id: String = asset_id.into();
1372 let summary = format!(
1373 "confidence revalidation failed for asset {asset_id} [{outcome:?}]: replay suspended"
1374 );
1375 ConfidenceRevalidationResult {
1376 revalidation_id: revalidation_id.into(),
1377 asset_id,
1378 confidence_state: prior_state,
1379 revalidation_result: outcome,
1380 replay_eligibility: ReplayEligibility::Ineligible,
1381 summary,
1382 fail_closed: true,
1383 }
1384}
1385
1386pub fn demote_asset(
1388 demotion_id: impl Into<String>,
1389 asset_id: impl Into<String>,
1390 prior_state: ConfidenceState,
1391 new_state: ConfidenceState,
1392 reason_code: ConfidenceDemotionReasonCode,
1393) -> DemotionDecision {
1394 let asset_id: String = asset_id.into();
1395 let quarantine_transition = new_state == ConfidenceState::Quarantined;
1396 let summary =
1397 format!("asset {asset_id} demoted from {prior_state:?} to {new_state:?} [{reason_code:?}]");
1398 DemotionDecision {
1399 demotion_id: demotion_id.into(),
1400 asset_id,
1401 prior_state,
1402 new_state,
1403 reason_code,
1404 replay_eligibility: ReplayEligibility::Ineligible,
1405 summary,
1406 quarantine_transition,
1407 fail_closed: true,
1408 }
1409}
1410
1411#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1412#[serde(rename_all = "snake_case")]
1413pub enum SelfEvolutionSelectionReasonCode {
1414 Accepted,
1415 IssueClosed,
1416 MissingEvolutionLabel,
1417 MissingFeatureLabel,
1418 ExcludedByLabel,
1419 UnsupportedCandidateScope,
1420 UnknownFailClosed,
1421}
1422
1423#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1424pub struct SelfEvolutionSelectionDecision {
1425 pub issue_number: u64,
1426 pub selected: bool,
1427 #[serde(default, skip_serializing_if = "Option::is_none")]
1428 pub candidate_class: Option<BoundedTaskClass>,
1429 pub summary: String,
1430 #[serde(default, skip_serializing_if = "Option::is_none")]
1431 pub reason_code: Option<SelfEvolutionSelectionReasonCode>,
1432 #[serde(default, skip_serializing_if = "Option::is_none")]
1433 pub failure_reason: Option<String>,
1434 #[serde(default, skip_serializing_if = "Option::is_none")]
1435 pub recovery_hint: Option<String>,
1436 pub fail_closed: bool,
1437}
1438
1439#[derive(Clone, Copy)]
1440struct SelfEvolutionSelectionDefaults {
1441 failure_reason: &'static str,
1442 recovery_hint: &'static str,
1443}
1444
1445fn self_evolution_selection_defaults(
1446 reason_code: &SelfEvolutionSelectionReasonCode,
1447) -> Option<SelfEvolutionSelectionDefaults> {
1448 match reason_code {
1449 SelfEvolutionSelectionReasonCode::Accepted => None,
1450 SelfEvolutionSelectionReasonCode::IssueClosed => Some(SelfEvolutionSelectionDefaults {
1451 failure_reason: "self-evolution candidate rejected because the issue is closed",
1452 recovery_hint: "Reopen the issue or choose an active open issue before retrying selection.",
1453 }),
1454 SelfEvolutionSelectionReasonCode::MissingEvolutionLabel => {
1455 Some(SelfEvolutionSelectionDefaults {
1456 failure_reason: "self-evolution candidate rejected because the issue is missing area/evolution",
1457 recovery_hint:
1458 "Add the area/evolution label or choose an issue already scoped to self-evolution.",
1459 })
1460 }
1461 SelfEvolutionSelectionReasonCode::MissingFeatureLabel => {
1462 Some(SelfEvolutionSelectionDefaults {
1463 failure_reason: "self-evolution candidate rejected because the issue is missing type/feature",
1464 recovery_hint:
1465 "Add the type/feature label or narrow the issue to a bounded feature slice before retrying.",
1466 })
1467 }
1468 SelfEvolutionSelectionReasonCode::ExcludedByLabel => Some(SelfEvolutionSelectionDefaults {
1469 failure_reason: "self-evolution candidate rejected by an excluded issue label",
1470 recovery_hint:
1471 "Remove the excluded label or choose a non-duplicate, non-invalid, actionable issue.",
1472 }),
1473 SelfEvolutionSelectionReasonCode::UnsupportedCandidateScope => {
1474 Some(SelfEvolutionSelectionDefaults {
1475 failure_reason:
1476 "self-evolution candidate rejected because the hinted file scope is outside the bounded docs policy",
1477 recovery_hint:
1478 "Narrow candidate paths to the approved docs/*.md boundary before retrying selection.",
1479 })
1480 }
1481 SelfEvolutionSelectionReasonCode::UnknownFailClosed => Some(SelfEvolutionSelectionDefaults {
1482 failure_reason: "self-evolution candidate failed with an unmapped selection reason",
1483 recovery_hint: "Unknown selection failure; fail closed and require explicit maintainer triage before retry.",
1484 }),
1485 }
1486}
1487
1488pub fn accept_self_evolution_selection_decision(
1489 issue_number: u64,
1490 candidate_class: BoundedTaskClass,
1491 summary: Option<&str>,
1492) -> SelfEvolutionSelectionDecision {
1493 let summary = normalize_optional_text(summary).unwrap_or_else(|| {
1494 format!("selected GitHub issue #{issue_number} as a bounded self-evolution candidate")
1495 });
1496 SelfEvolutionSelectionDecision {
1497 issue_number,
1498 selected: true,
1499 candidate_class: Some(candidate_class),
1500 summary,
1501 reason_code: Some(SelfEvolutionSelectionReasonCode::Accepted),
1502 failure_reason: None,
1503 recovery_hint: None,
1504 fail_closed: false,
1505 }
1506}
1507
1508pub fn reject_self_evolution_selection_decision(
1509 issue_number: u64,
1510 reason_code: SelfEvolutionSelectionReasonCode,
1511 failure_reason: Option<&str>,
1512 summary: Option<&str>,
1513) -> SelfEvolutionSelectionDecision {
1514 let defaults = self_evolution_selection_defaults(&reason_code)
1515 .unwrap_or(SelfEvolutionSelectionDefaults {
1516 failure_reason: "self-evolution candidate rejected",
1517 recovery_hint:
1518 "Review candidate selection inputs and retry within the bounded self-evolution policy.",
1519 });
1520 let failure_reason = normalize_optional_text(failure_reason)
1521 .unwrap_or_else(|| defaults.failure_reason.to_string());
1522 let reason_code_key = match reason_code {
1523 SelfEvolutionSelectionReasonCode::Accepted => "accepted",
1524 SelfEvolutionSelectionReasonCode::IssueClosed => "issue_closed",
1525 SelfEvolutionSelectionReasonCode::MissingEvolutionLabel => "missing_evolution_label",
1526 SelfEvolutionSelectionReasonCode::MissingFeatureLabel => "missing_feature_label",
1527 SelfEvolutionSelectionReasonCode::ExcludedByLabel => "excluded_by_label",
1528 SelfEvolutionSelectionReasonCode::UnsupportedCandidateScope => {
1529 "unsupported_candidate_scope"
1530 }
1531 SelfEvolutionSelectionReasonCode::UnknownFailClosed => "unknown_fail_closed",
1532 };
1533 let summary = normalize_optional_text(summary).unwrap_or_else(|| {
1534 format!(
1535 "rejected GitHub issue #{issue_number} as a self-evolution candidate [{reason_code_key}]"
1536 )
1537 });
1538
1539 SelfEvolutionSelectionDecision {
1540 issue_number,
1541 selected: false,
1542 candidate_class: None,
1543 summary,
1544 reason_code: Some(reason_code),
1545 failure_reason: Some(failure_reason),
1546 recovery_hint: Some(defaults.recovery_hint.to_string()),
1547 fail_closed: true,
1548 }
1549}
1550
1551pub fn accept_discovered_candidate(
1552 dedupe_key: impl Into<String>,
1553 candidate_source: AutonomousCandidateSource,
1554 candidate_class: BoundedTaskClass,
1555 signals: Vec<String>,
1556 summary: Option<&str>,
1557) -> DiscoveredCandidate {
1558 let summary = normalize_optional_text(summary)
1559 .unwrap_or_else(|| format!("accepted autonomous candidate from {candidate_source:?}"));
1560 DiscoveredCandidate {
1561 dedupe_key: dedupe_key.into(),
1562 candidate_source,
1563 candidate_class: Some(candidate_class),
1564 signals,
1565 accepted: true,
1566 reason_code: AutonomousIntakeReasonCode::Accepted,
1567 summary,
1568 failure_reason: None,
1569 recovery_hint: None,
1570 fail_closed: false,
1571 }
1572}
1573
1574pub fn deny_discovered_candidate(
1575 dedupe_key: impl Into<String>,
1576 candidate_source: AutonomousCandidateSource,
1577 signals: Vec<String>,
1578 reason_code: AutonomousIntakeReasonCode,
1579) -> DiscoveredCandidate {
1580 let (failure_reason, recovery_hint) = match reason_code {
1581 AutonomousIntakeReasonCode::UnsupportedSignalClass => (
1582 "signal class is not supported by the bounded evolution policy",
1583 "review supported candidate signal classes and filter input before retry",
1584 ),
1585 AutonomousIntakeReasonCode::AmbiguousSignal => (
1586 "signals do not map to a unique bounded candidate class",
1587 "provide more specific signal tokens or triage manually before resubmitting",
1588 ),
1589 AutonomousIntakeReasonCode::DuplicateCandidate => (
1590 "an equivalent candidate has already been discovered in this intake window",
1591 "deduplicate signals before resubmitting or check the existing candidate queue",
1592 ),
1593 AutonomousIntakeReasonCode::UnknownFailClosed => (
1594 "candidate intake failed with an unmapped reason; fail closed",
1595 "require explicit maintainer triage before retry",
1596 ),
1597 AutonomousIntakeReasonCode::Accepted => (
1598 "unexpected accepted reason on deny path",
1599 "use accept_discovered_candidate for accepted outcomes",
1600 ),
1601 };
1602 let summary =
1603 format!("denied autonomous candidate from {candidate_source:?}: {failure_reason}");
1604 DiscoveredCandidate {
1605 dedupe_key: dedupe_key.into(),
1606 candidate_source,
1607 candidate_class: None,
1608 signals,
1609 accepted: false,
1610 reason_code,
1611 summary,
1612 failure_reason: Some(failure_reason.to_string()),
1613 recovery_hint: Some(recovery_hint.to_string()),
1614 fail_closed: true,
1615 }
1616}
1617
1618#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1619pub struct HumanApproval {
1620 pub approved: bool,
1621 pub approver: Option<String>,
1622 pub note: Option<String>,
1623}
1624
1625#[derive(Clone, Debug, Serialize, Deserialize)]
1626pub struct SupervisedDevloopRequest {
1627 pub task: AgentTask,
1628 pub proposal: MutationProposal,
1629 pub approval: HumanApproval,
1630}
1631
1632#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1633pub enum SupervisedDevloopStatus {
1634 AwaitingApproval,
1635 RejectedByPolicy,
1636 FailedClosed,
1637 Executed,
1638}
1639
1640#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1641#[serde(rename_all = "snake_case")]
1642pub enum SupervisedDeliveryStatus {
1643 Prepared,
1644 Denied,
1645}
1646
1647#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1648#[serde(rename_all = "snake_case")]
1649pub enum SupervisedDeliveryApprovalState {
1650 Approved,
1651 MissingExplicitApproval,
1652}
1653
1654#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1655#[serde(rename_all = "snake_case")]
1656pub enum SupervisedDeliveryReasonCode {
1657 DeliveryPrepared,
1658 AwaitingApproval,
1659 DeliveryEvidenceMissing,
1660 ValidationEvidenceMissing,
1661 UnsupportedTaskScope,
1662 InconsistentDeliveryEvidence,
1663 UnknownFailClosed,
1664}
1665
1666#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1667pub struct SupervisedDeliveryContract {
1668 pub delivery_summary: String,
1669 #[serde(default, skip_serializing_if = "Option::is_none")]
1670 pub branch_name: Option<String>,
1671 #[serde(default, skip_serializing_if = "Option::is_none")]
1672 pub pr_title: Option<String>,
1673 #[serde(default, skip_serializing_if = "Option::is_none")]
1674 pub pr_summary: Option<String>,
1675 pub delivery_status: SupervisedDeliveryStatus,
1676 pub approval_state: SupervisedDeliveryApprovalState,
1677 pub reason_code: SupervisedDeliveryReasonCode,
1678 #[serde(default)]
1679 pub fail_closed: bool,
1680 #[serde(default, skip_serializing_if = "Option::is_none")]
1681 pub recovery_hint: Option<String>,
1682}
1683
1684#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1685#[serde(rename_all = "snake_case")]
1686pub enum SupervisedExecutionDecision {
1687 AwaitingApproval,
1688 ReplayHit,
1689 PlannerFallback,
1690 RejectedByPolicy,
1691 FailedClosed,
1692}
1693
1694#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1695#[serde(rename_all = "snake_case")]
1696pub enum SupervisedValidationOutcome {
1697 NotRun,
1698 Passed,
1699 FailedClosed,
1700}
1701
1702#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1703#[serde(rename_all = "snake_case")]
1704pub enum SupervisedExecutionReasonCode {
1705 AwaitingHumanApproval,
1706 ReplayHit,
1707 ReplayFallback,
1708 PolicyDenied,
1709 ValidationFailed,
1710 UnsafePatch,
1711 Timeout,
1712 MutationPayloadMissing,
1713 UnknownFailClosed,
1714}
1715#[derive(Clone, Debug, Serialize, Deserialize)]
1716pub struct SupervisedDevloopOutcome {
1717 pub task_id: String,
1718 pub task_class: Option<BoundedTaskClass>,
1719 pub status: SupervisedDevloopStatus,
1720 pub execution_decision: SupervisedExecutionDecision,
1721 #[serde(default, skip_serializing_if = "Option::is_none")]
1722 pub replay_outcome: Option<ReplayFeedback>,
1723 #[serde(default, skip_serializing_if = "Option::is_none")]
1724 pub fallback_reason: Option<String>,
1725 pub validation_outcome: SupervisedValidationOutcome,
1726 pub evidence_summary: String,
1727 #[serde(default, skip_serializing_if = "Option::is_none")]
1728 pub reason_code: Option<SupervisedExecutionReasonCode>,
1729 #[serde(default, skip_serializing_if = "Option::is_none")]
1730 pub recovery_hint: Option<String>,
1731 pub execution_feedback: Option<ExecutionFeedback>,
1732 #[serde(default, skip_serializing_if = "Option::is_none")]
1733 pub failure_contract: Option<MutationNeededFailureContract>,
1734 pub summary: String,
1735}
1736
1737#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1738#[serde(rename_all = "snake_case")]
1739pub enum SelfEvolutionAuditConsistencyResult {
1740 Consistent,
1741 Inconsistent,
1742}
1743
1744#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1745#[serde(rename_all = "snake_case")]
1746pub enum SelfEvolutionAcceptanceGateReasonCode {
1747 Accepted,
1748 MissingSelectionEvidence,
1749 MissingProposalEvidence,
1750 MissingApprovalEvidence,
1751 MissingExecutionEvidence,
1752 MissingDeliveryEvidence,
1753 InconsistentReasonCodeMatrix,
1754 UnknownFailClosed,
1755}
1756
1757#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1758pub struct SelfEvolutionApprovalEvidence {
1759 pub approval_required: bool,
1760 pub approved: bool,
1761 #[serde(default, skip_serializing_if = "Option::is_none")]
1762 pub approver: Option<String>,
1763}
1764
1765#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1766pub struct SelfEvolutionDeliveryOutcome {
1767 pub delivery_status: SupervisedDeliveryStatus,
1768 pub approval_state: SupervisedDeliveryApprovalState,
1769 pub reason_code: SupervisedDeliveryReasonCode,
1770}
1771
1772#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1773pub struct SelfEvolutionReasonCodeMatrix {
1774 #[serde(default, skip_serializing_if = "Option::is_none")]
1775 pub selection_reason_code: Option<SelfEvolutionSelectionReasonCode>,
1776 pub proposal_reason_code: MutationProposalContractReasonCode,
1777 #[serde(default, skip_serializing_if = "Option::is_none")]
1778 pub execution_reason_code: Option<SupervisedExecutionReasonCode>,
1779 pub delivery_reason_code: SupervisedDeliveryReasonCode,
1780}
1781
1782#[derive(Clone, Debug, Serialize, Deserialize)]
1783pub struct SelfEvolutionAcceptanceGateInput {
1784 pub selection_decision: SelfEvolutionSelectionDecision,
1785 pub proposal_contract: SelfEvolutionMutationProposalContract,
1786 pub supervised_request: SupervisedDevloopRequest,
1787 pub execution_outcome: SupervisedDevloopOutcome,
1788 pub delivery_contract: SupervisedDeliveryContract,
1789}
1790
1791#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1792pub struct SelfEvolutionAcceptanceGateContract {
1793 pub acceptance_gate_summary: String,
1794 pub audit_consistency_result: SelfEvolutionAuditConsistencyResult,
1795 pub approval_evidence: SelfEvolutionApprovalEvidence,
1796 pub delivery_outcome: SelfEvolutionDeliveryOutcome,
1797 pub reason_code_matrix: SelfEvolutionReasonCodeMatrix,
1798 pub fail_closed: bool,
1799 pub reason_code: SelfEvolutionAcceptanceGateReasonCode,
1800 #[serde(default, skip_serializing_if = "Option::is_none")]
1801 pub recovery_hint: Option<String>,
1802}
1803
1804#[cfg(test)]
1805mod tests {
1806 use super::*;
1807
1808 fn handshake_request_with_versions(versions: &[&str]) -> A2aHandshakeRequest {
1809 A2aHandshakeRequest {
1810 agent_id: "agent-test".into(),
1811 role: AgentRole::Planner,
1812 capability_level: AgentCapabilityLevel::A2,
1813 supported_protocols: versions
1814 .iter()
1815 .map(|version| A2aProtocol {
1816 name: A2A_PROTOCOL_NAME.into(),
1817 version: (*version).into(),
1818 })
1819 .collect(),
1820 advertised_capabilities: vec![A2aCapability::Coordination],
1821 }
1822 }
1823
1824 #[test]
1825 fn negotiate_supported_protocol_prefers_v1_when_available() {
1826 let req = handshake_request_with_versions(&[A2A_PROTOCOL_VERSION, A2A_PROTOCOL_VERSION_V1]);
1827 let negotiated = req
1828 .negotiate_supported_protocol()
1829 .expect("expected protocol negotiation success");
1830 assert_eq!(negotiated.name, A2A_PROTOCOL_NAME);
1831 assert_eq!(negotiated.version, A2A_PROTOCOL_VERSION_V1);
1832 }
1833
1834 #[test]
1835 fn negotiate_supported_protocol_falls_back_to_experimental() {
1836 let req = handshake_request_with_versions(&[A2A_PROTOCOL_VERSION]);
1837 let negotiated = req
1838 .negotiate_supported_protocol()
1839 .expect("expected protocol negotiation success");
1840 assert_eq!(negotiated.version, A2A_PROTOCOL_VERSION);
1841 }
1842
1843 #[test]
1844 fn negotiate_supported_protocol_returns_none_without_overlap() {
1845 let req = handshake_request_with_versions(&["0.0.1"]);
1846 assert!(req.negotiate_supported_protocol().is_none());
1847 }
1848
1849 #[test]
1850 fn normalize_replay_fallback_contract_maps_known_reason() {
1851 let contract = normalize_replay_fallback_contract(
1852 &ReplayPlannerDirective::PlanFallback,
1853 Some("no matching gene"),
1854 None,
1855 None,
1856 None,
1857 None,
1858 )
1859 .expect("contract should exist");
1860
1861 assert_eq!(
1862 contract.reason_code,
1863 ReplayFallbackReasonCode::NoCandidateAfterSelect
1864 );
1865 assert_eq!(
1866 contract.next_action,
1867 ReplayFallbackNextAction::PlanFromScratch
1868 );
1869 assert_eq!(contract.confidence, 92);
1870 }
1871
1872 #[test]
1873 fn normalize_replay_fallback_contract_fails_closed_for_unknown_reason() {
1874 let contract = normalize_replay_fallback_contract(
1875 &ReplayPlannerDirective::PlanFallback,
1876 Some("something unexpected"),
1877 None,
1878 None,
1879 None,
1880 None,
1881 )
1882 .expect("contract should exist");
1883
1884 assert_eq!(
1885 contract.reason_code,
1886 ReplayFallbackReasonCode::UnmappedFallbackReason
1887 );
1888 assert_eq!(
1889 contract.next_action,
1890 ReplayFallbackNextAction::EscalateFailClosed
1891 );
1892 assert_eq!(contract.confidence, 0);
1893 }
1894
1895 #[test]
1896 fn normalize_replay_fallback_contract_rejects_conflicting_next_action() {
1897 let contract = normalize_replay_fallback_contract(
1898 &ReplayPlannerDirective::PlanFallback,
1899 Some("replay validation failed"),
1900 Some(ReplayFallbackReasonCode::ValidationFailed),
1901 None,
1902 Some(ReplayFallbackNextAction::PlanFromScratch),
1903 Some(88),
1904 )
1905 .expect("contract should exist");
1906
1907 assert_eq!(
1908 contract.reason_code,
1909 ReplayFallbackReasonCode::UnmappedFallbackReason
1910 );
1911 assert_eq!(
1912 contract.next_action,
1913 ReplayFallbackNextAction::EscalateFailClosed
1914 );
1915 assert_eq!(contract.confidence, 0);
1916 }
1917
1918 #[test]
1919 fn normalize_mutation_needed_failure_contract_maps_policy_denied() {
1920 let contract = normalize_mutation_needed_failure_contract(
1921 Some("supervised devloop rejected task because it is outside bounded scope"),
1922 None,
1923 );
1924
1925 assert_eq!(
1926 contract.reason_code,
1927 MutationNeededFailureReasonCode::PolicyDenied
1928 );
1929 assert_eq!(
1930 contract.recovery_action,
1931 MutationNeededRecoveryAction::NarrowScopeAndRetry
1932 );
1933 assert!(contract.fail_closed);
1934 }
1935
1936 #[test]
1937 fn normalize_mutation_needed_failure_contract_maps_timeout() {
1938 let contract = normalize_mutation_needed_failure_contract(
1939 Some("command timed out: git apply --check patch.diff"),
1940 None,
1941 );
1942
1943 assert_eq!(
1944 contract.reason_code,
1945 MutationNeededFailureReasonCode::Timeout
1946 );
1947 assert_eq!(
1948 contract.recovery_action,
1949 MutationNeededRecoveryAction::ReduceExecutionBudget
1950 );
1951 assert!(contract.fail_closed);
1952 }
1953
1954 #[test]
1955 fn normalize_mutation_needed_failure_contract_fails_closed_for_unknown_reason() {
1956 let contract =
1957 normalize_mutation_needed_failure_contract(Some("unexpected runner panic"), None);
1958
1959 assert_eq!(
1960 contract.reason_code,
1961 MutationNeededFailureReasonCode::UnknownFailClosed
1962 );
1963 assert_eq!(
1964 contract.recovery_action,
1965 MutationNeededRecoveryAction::EscalateFailClosed
1966 );
1967 assert!(contract.fail_closed);
1968 }
1969
1970 #[test]
1971 fn reject_self_evolution_selection_decision_maps_closed_issue_defaults() {
1972 let decision = reject_self_evolution_selection_decision(
1973 234,
1974 SelfEvolutionSelectionReasonCode::IssueClosed,
1975 None,
1976 None,
1977 );
1978
1979 assert!(!decision.selected);
1980 assert_eq!(decision.issue_number, 234);
1981 assert_eq!(
1982 decision.reason_code,
1983 Some(SelfEvolutionSelectionReasonCode::IssueClosed)
1984 );
1985 assert!(decision.fail_closed);
1986 assert!(decision
1987 .failure_reason
1988 .as_deref()
1989 .is_some_and(|reason| reason.contains("closed")));
1990 assert!(decision.recovery_hint.is_some());
1991 }
1992
1993 #[test]
1994 fn accept_self_evolution_selection_decision_marks_candidate_selected() {
1995 let decision =
1996 accept_self_evolution_selection_decision(235, BoundedTaskClass::DocsSingleFile, None);
1997
1998 assert!(decision.selected);
1999 assert_eq!(decision.issue_number, 235);
2000 assert_eq!(
2001 decision.candidate_class,
2002 Some(BoundedTaskClass::DocsSingleFile)
2003 );
2004 assert_eq!(
2005 decision.reason_code,
2006 Some(SelfEvolutionSelectionReasonCode::Accepted)
2007 );
2008 assert!(!decision.fail_closed);
2009 assert_eq!(decision.failure_reason, None);
2010 assert_eq!(decision.recovery_hint, None);
2011 }
2012}
2013
2014#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
2016pub enum HubTrustTier {
2017 Full,
2019 ReadOnly,
2021}
2022
2023#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
2025pub enum HubOperationClass {
2026 Hello,
2027 Fetch,
2028 Publish,
2029 Revoke,
2030 TaskClaim,
2031 TaskComplete,
2032 WorkerRegister,
2033 Recipe,
2034 Session,
2035 Dispute,
2036 Swarm,
2037}
2038
2039impl HubOperationClass {
2040 pub fn is_read_only(&self) -> bool {
2042 matches!(self, HubOperationClass::Hello | HubOperationClass::Fetch)
2043 }
2044}
2045
2046#[derive(Clone, Debug, Serialize, Deserialize)]
2048pub struct HubProfile {
2049 pub hub_id: String,
2050 pub base_url: String,
2051 pub trust_tier: HubTrustTier,
2052 pub priority: u32,
2054 pub health_url: Option<String>,
2056}
2057
2058impl HubProfile {
2059 pub fn allows_operation(&self, operation: &HubOperationClass) -> bool {
2061 match &self.trust_tier {
2062 HubTrustTier::Full => true,
2063 HubTrustTier::ReadOnly => operation.is_read_only(),
2064 }
2065 }
2066}
2067
2068#[derive(Clone, Debug)]
2070pub struct HubSelectionPolicy {
2071 pub allowed_tiers_for_operation: Vec<(HubOperationClass, Vec<HubTrustTier>)>,
2073 pub default_allowed_tiers: Vec<HubTrustTier>,
2075}
2076
2077impl Default for HubSelectionPolicy {
2078 fn default() -> Self {
2079 Self {
2080 allowed_tiers_for_operation: vec![
2081 (
2082 HubOperationClass::Hello,
2083 vec![HubTrustTier::Full, HubTrustTier::ReadOnly],
2084 ),
2085 (
2086 HubOperationClass::Fetch,
2087 vec![HubTrustTier::Full, HubTrustTier::ReadOnly],
2088 ),
2089 (HubOperationClass::Publish, vec![HubTrustTier::Full]),
2091 (HubOperationClass::Revoke, vec![HubTrustTier::Full]),
2092 (HubOperationClass::TaskClaim, vec![HubTrustTier::Full]),
2093 (HubOperationClass::TaskComplete, vec![HubTrustTier::Full]),
2094 (HubOperationClass::WorkerRegister, vec![HubTrustTier::Full]),
2095 (HubOperationClass::Recipe, vec![HubTrustTier::Full]),
2096 (HubOperationClass::Session, vec![HubTrustTier::Full]),
2097 (HubOperationClass::Dispute, vec![HubTrustTier::Full]),
2098 (HubOperationClass::Swarm, vec![HubTrustTier::Full]),
2099 ],
2100 default_allowed_tiers: vec![HubTrustTier::Full],
2101 }
2102 }
2103}
2104
2105impl HubSelectionPolicy {
2106 pub fn allowed_tiers(&self, operation: &HubOperationClass) -> &[HubTrustTier] {
2108 self.allowed_tiers_for_operation
2109 .iter()
2110 .find(|(op, _)| op == operation)
2111 .map(|(_, tiers)| tiers.as_slice())
2112 .unwrap_or(&self.default_allowed_tiers)
2113 }
2114}