Skip to content

Commit 3ff031d

Browse files
authored
fix: publish authenticated external adoptions (#9062)
1 parent 7f59ba9 commit 3ff031d

2 files changed

Lines changed: 140 additions & 1 deletion

File tree

crates/homeboy-agents/src/agent_task_finalization.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@ fn report(
521521
enum DurablePublicationEligibility {
522522
ProviderRun,
523523
PreProviderCandidateAdoptionRecovery,
524+
AuthenticatedExternalCandidateAdoption,
524525
}
525526

526527
fn validate_durable_publication_eligibility(
@@ -566,7 +567,46 @@ fn validate_durable_publication_eligibility(
566567
return Ok(DurablePublicationEligibility::PreProviderCandidateAdoptionRecovery);
567568
}
568569

569-
Err(Error::validation_invalid_argument("run_id", "durable run must have succeeded execution and succeeded provider runtime before publication; the only exception is an applied, green, fingerprinted candidate-adoption recovery with durable zero-execution pre-provider transport provenance", None, None))
570+
// An externally prepared commit has no successful provider runtime to
571+
// attest. Its authenticated adoption promotion supplies equivalent,
572+
// candidate-bound evidence instead.
573+
let committed_change_provenance = promotion.provenance["change_source"] == "local_commits"
574+
&& promotion
575+
.provenance
576+
.get("commit_range")
577+
.and_then(serde_json::Value::as_str)
578+
.and_then(|range| range.rsplit_once(".."))
579+
.is_some_and(|(_, candidate)| Some(candidate) == candidate_head)
580+
&& promotion
581+
.provenance
582+
.get("commits")
583+
.and_then(serde_json::Value::as_array)
584+
.is_some_and(|commits| !commits.is_empty());
585+
let candidate_is_bound = candidate_ref.is_some_and(|candidate_ref| {
586+
is_git_commit_identity(candidate_ref)
587+
&& candidate_head.is_some_and(|candidate_head| {
588+
is_full_git_commit_identity(candidate_head)
589+
&& (candidate_ref == candidate_head
590+
|| candidate_head.starts_with(candidate_ref))
591+
})
592+
});
593+
let authenticated_external_adoption = promotion.status
594+
== crate::agent_task_promotion::AgentTaskPromotionStatus::Applied
595+
&& promotion.provenance["adoption"]["source_run_id"]
596+
== promotion.source.run_id.clone().unwrap_or_default()
597+
&& candidate_is_bound
598+
&& adoption_model.is_some_and(is_concrete_model)
599+
&& committed_change_provenance
600+
&& !promotion.gate_results.is_empty()
601+
&& promotion
602+
.gate_results
603+
.iter()
604+
.all(|gate| gate.status == HomeboyGateStatus::Passed);
605+
if authenticated_external_adoption {
606+
return Ok(DurablePublicationEligibility::AuthenticatedExternalCandidateAdoption);
607+
}
608+
609+
Err(Error::validation_invalid_argument("run_id", "durable run must have succeeded execution and succeeded provider runtime before publication; the only exceptions are an applied, green, fingerprinted candidate-adoption recovery with durable zero-execution pre-provider transport provenance or an applied, green, committed-change-provenance-bound authenticated external candidate adoption", None, None))
570610
}
571611

572612
fn is_concrete_model(value: &str) -> bool {
@@ -584,6 +624,10 @@ fn is_concrete_model(value: &str) -> bool {
584624
}
585625

586626
fn is_git_commit_identity(value: &str) -> bool {
627+
(7..=64).contains(&value.len()) && value.bytes().all(|byte| byte.is_ascii_hexdigit())
628+
}
629+
630+
fn is_full_git_commit_identity(value: &str) -> bool {
587631
matches!(value.len(), 40 | 64) && value.bytes().all(|byte| byte.is_ascii_hexdigit())
588632
}
589633

crates/homeboy-agents/src/agent_task_finalization/tests.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,90 @@ fn durable_finalization_accepts_only_authenticated_pre_provider_candidate_adopti
895895
rejected(recovery_lifecycle, non_green);
896896
}
897897

898+
#[test]
899+
fn durable_finalization_accepts_only_authenticated_external_candidate_adoption() {
900+
let partial_lifecycle = RunLifecycleRecord {
901+
execution: RunExecutionLifecycle {
902+
state: RunExecutionState::PartialFailure,
903+
started_at: None,
904+
finished_at: Some("2026-01-01T00:00:00Z".to_string()),
905+
updated_at: None,
906+
},
907+
..RunLifecycleRecord::default()
908+
};
909+
let finalize = |gate_proof: AgentTaskPrDurableGateProof| {
910+
let mut backend = MockBackend {
911+
changed_files: vec!["src/lib.rs".to_string()],
912+
lifecycle: Some(partial_lifecycle.clone()),
913+
gate_proof: Some(gate_proof),
914+
..Default::default()
915+
};
916+
let mut finalization_options = options();
917+
finalization_options.manual_finalization = false;
918+
finalization_options.changed_files = vec!["src/lib.rs".to_string()];
919+
let report = finalize_pr_with_backend(finalization_options, &mut backend);
920+
(report, backend)
921+
};
922+
923+
let mut accepted_proof = external_adoption_gate_proof();
924+
accepted_proof.promotion.changed_files = vec!["src/lib.rs".to_string()];
925+
let (report, backend) = finalize(accepted_proof);
926+
assert_eq!(
927+
report.expect("authenticated adoption publishes").status,
928+
"review_ready"
929+
);
930+
assert!(backend.committed && backend.pushed && backend.created);
931+
932+
let mut abbreviated_proof = external_adoption_gate_proof();
933+
abbreviated_proof.promotion.changed_files = vec!["src/lib.rs".to_string()];
934+
abbreviated_proof.promotion.provenance["adoption"]["candidate_ref"] = json!("7f76933ef");
935+
let (report, backend) = finalize(abbreviated_proof);
936+
assert_eq!(
937+
report
938+
.expect("fingerprinted abbreviated candidate publishes")
939+
.status,
940+
"review_ready"
941+
);
942+
assert!(backend.committed && backend.pushed && backend.created);
943+
944+
let rejected = |proof: AgentTaskPrDurableGateProof| {
945+
let (result, backend) = finalize(proof);
946+
assert!(result.is_err());
947+
assert!(!backend.committed && !backend.pushed && !backend.created);
948+
};
949+
950+
rejected(successful_gate_proof());
951+
952+
let mut missing_candidate = external_adoption_gate_proof();
953+
missing_candidate.promotion.provenance["candidate"] = serde_json::Value::Null;
954+
rejected(missing_candidate);
955+
956+
let mut mismatched_candidate = external_adoption_gate_proof();
957+
mismatched_candidate.promotion.provenance["candidate"]["fingerprint"]["head"] =
958+
json!("0000000000000000000000000000000000000000");
959+
rejected(mismatched_candidate);
960+
961+
let mut non_prefix_candidate = external_adoption_gate_proof();
962+
non_prefix_candidate.promotion.provenance["adoption"]["candidate_ref"] = json!("7f76933e0");
963+
rejected(non_prefix_candidate);
964+
965+
let mut missing_model = external_adoption_gate_proof();
966+
missing_model.promotion.provenance["adoption"]["ai_model"] = serde_json::Value::Null;
967+
rejected(missing_model);
968+
969+
let mut mismatched_source = external_adoption_gate_proof();
970+
mismatched_source.promotion.provenance["adoption"]["source_run_id"] = json!("other-run");
971+
rejected(mismatched_source);
972+
973+
let mut missing_commit_binding = external_adoption_gate_proof();
974+
missing_commit_binding.promotion.provenance["commit_range"] = json!("base..other");
975+
rejected(missing_commit_binding);
976+
977+
let mut non_green = external_adoption_gate_proof();
978+
non_green.promotion.gate_results[0].status = HomeboyGateStatus::Failed;
979+
rejected(non_green);
980+
}
981+
898982
#[test]
899983
fn durable_finalization_publishes_clean_synced_recovered_candidate() {
900984
let mut gate_proof = successful_gate_proof();
@@ -1546,6 +1630,17 @@ fn pre_provider_adoption_gate_proof() -> AgentTaskPrDurableGateProof {
15461630
proof
15471631
}
15481632

1633+
fn external_adoption_gate_proof() -> AgentTaskPrDurableGateProof {
1634+
let mut proof = pre_provider_adoption_gate_proof();
1635+
proof.promotion.provenance["adoption"]["recovery"] = serde_json::Value::Null;
1636+
proof.promotion.provenance["change_source"] = json!("local_commits");
1637+
proof.promotion.provenance["commit_range"] =
1638+
json!("base..7f76933ef002d195ee1cc5bf21069e0f40b1c972");
1639+
proof.promotion.provenance["commits"] =
1640+
json!([{"sha": "7f76933ef002d195ee1cc5bf21069e0f40b1c972"}]);
1641+
proof
1642+
}
1643+
15491644
#[test]
15501645
fn validates_publication_intent_contract() {
15511646
let mut backend = MockBackend {

0 commit comments

Comments
 (0)