Skip to main content

lean_ctx/core/execution_ledger/host/
context_protocol.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Authenticate the genuine planning record against the actual published native run.
4
5use lean_ctx_ocla::DecisionSignerAdmissionV1;
6use lean_ctx_protocol::{
7    AcceptanceState, DecisionKind, DecisionStageV1, EngineInvocationV1, EngineObservationV1,
8};
9
10use super::{HostReceiptAttempt, HostReceiptAuthority, PublishedCanonicalReceipt, digest, now};
11use crate::core::{
12    canonical::canonical_serialize,
13    execution_protocol::RecordedExecutionProtocolV1,
14    outcome::{contracts::OutcomeContractV1, evaluator::evaluate_for_task},
15};
16
17impl HostReceiptAuthority {
18    /// Initial publication has no quality signal; never derive acceptance from success.
19    pub fn published_context_protocol(
20        &self,
21        attempt: &HostReceiptAttempt,
22        invocation: &EngineInvocationV1,
23        observation: &EngineObservationV1,
24        published: &PublishedCanonicalReceipt,
25    ) -> Result<Option<RecordedExecutionProtocolV1>, String> {
26        if !self.allow_context_decision_signing {
27            return Ok(None);
28        }
29        let context = attempt
30            .context
31            .as_ref()
32            .ok_or("host_context_decision_missing")?;
33        let decision = attempt
34            .context_decision
35            .as_ref()
36            .ok_or("host_context_decision_missing")?;
37        let (receipt, _) = published
38            .read_canonical()
39            .map_err(|error| error.to_string())?;
40        let identity = digest(&canonical_serialize(&(
41            "native-unobserved-outcome-v1",
42            &attempt.task,
43            &receipt.receipt_id,
44            &decision.decision_id,
45        )))?;
46        let mut outcome = evaluate_for_task(
47            &OutcomeContractV1::for_task_class(context.decision().task_class),
48            &[],
49            format!("outcome:{}", identity.hex()),
50            attempt.task.task_id.as_str(),
51            now()?.as_str(),
52        );
53        if outcome.accepted != AcceptanceState::Unknown || outcome.quality_score_milli.is_some() {
54            return Err("native publication has no accepted outcome evidence".into());
55        }
56        outcome.plan_id = Some(attempt.plan.plan_id.clone());
57        outcome.receipt_id = Some(
58            lean_ctx_protocol::ReceiptId::new(receipt.receipt_id.as_str())
59                .map_err(|error| error.to_string())?,
60        );
61        outcome.decision_refs = vec![decision.decision_id.as_str().into()];
62        let protocol = RecordedExecutionProtocolV1 {
63            context_plan: context.context_projection().clone(),
64            execution_plan: attempt.plan.clone(),
65            invocation: invocation.clone(),
66            observation: observation.clone(),
67            receipt,
68            published_receipt: published.clone(),
69            accepted_outcome: outcome,
70            decision: decision.clone(),
71        };
72        let grant = DecisionSignerAdmissionV1 {
73            key_admission: self.signer_admission.clone(),
74            task: attempt.task.clone(),
75            stage: DecisionStageV1::Planning,
76            kind: DecisionKind::ContextSelection,
77        };
78        let key = self.signing_key.verifying_key();
79        protocol
80            .validated_for(
81                &attempt.task,
82                (&self.signer_admission, &key),
83                (&grant, &key),
84                Some(context),
85                &now()?,
86            )
87            .map_err(|error| error.to_string())?;
88        Ok(Some(protocol))
89    }
90}