use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::receipt::{
EvaluationIdentity, EvaluationReceipt, EVALUATION_IDENTITY_CONTRACT, EVALUATION_RECEIPT_SCHEMA,
};
use super::{contract::DecisionContract, question::QuestionSet, Evaluation, EvaluationPolicy};
use crate::value::{VmError, VmValue};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EvaluationRequest {
pub site_id: String,
pub state: Value,
pub questions: Value,
pub policy: Value,
}
impl EvaluationRequest {
pub(super) fn arguments(&self) -> [VmValue; 4] {
[
VmValue::string(&self.site_id),
crate::schema::json_to_vm_value(&self.state),
crate::schema::json_to_vm_value(&self.questions),
crate::schema::json_to_vm_value(&self.policy),
]
}
}
pub(super) fn request_identity(
state: &Value,
questions: &QuestionSet,
policy: &EvaluationPolicy,
route: Option<&DecisionContract>,
) -> EvaluationIdentity {
let canonical_state = crate::canonical_json::to_vec(state);
EvaluationIdentity {
contract_version: EVALUATION_IDENTITY_CONTRACT.into(),
structured_output_strategy: route
.and_then(|route| route.structured_output_strategy)
.map(|strategy| strategy.as_str().to_string()),
input_digest: super::digest_parts(std::iter::once(
std::str::from_utf8(&canonical_state).expect("canonical JSON is UTF-8"),
)),
canonical_input_type: super::state_type_name(state).into(),
question_set_digest: super::question_set_digest(questions),
policy_digest: policy.digest(),
evaluator_instruction_version: if route.is_some_and(|route| route.protocol.is_native()) {
"harn.evaluator.native.v2".into()
} else {
super::structured::EVALUATOR_INSTRUCTION_VERSION.into()
},
output_schema_version: super::structured::OUTPUT_SCHEMA_VERSION.into(),
backend_kind: policy.backend.as_str().into(),
protocol: route
.map(|route| route.protocol.as_str().to_string())
.unwrap_or_else(|| "unresolved".into()),
}
}
pub(super) fn request_id(site_id: &str, identity: &EvaluationIdentity) -> String {
let encoded = serde_json::to_string(identity).expect("evaluation identity serializes");
super::digest_parts([site_id, encoded.as_str()].into_iter())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VerificationRefusalCode {
UnsupportedContract,
InputMismatch,
QuestionsMismatch,
PolicyMismatch,
SiteMismatch,
RouteMismatch,
IdentityMismatch,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VerificationRefusal {
pub code: VerificationRefusalCode,
pub field: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReceiptVerification {
pub schema: String,
pub verified: bool,
pub refusals: Vec<VerificationRefusal>,
pub request_identity: Option<EvaluationIdentity>,
pub stable_request_id: Option<String>,
}
pub fn verify_receipt(
request: &EvaluationRequest,
receipt: &EvaluationReceipt,
) -> Result<ReceiptVerification, VmError> {
use VerificationRefusalCode as Code;
let mut report = ReceiptVerification {
schema: "harn.evaluation_verification.v1".into(),
verified: false,
refusals: Vec::new(),
request_identity: None,
stable_request_id: None,
};
let mut check = |matches: bool, code, field: &str| {
if !matches {
report.refusals.push(VerificationRefusal {
code,
field: field.into(),
});
}
};
check(
receipt.schema == EVALUATION_RECEIPT_SCHEMA,
Code::UnsupportedContract,
"schema",
);
check(
receipt.identity.contract_version == EVALUATION_IDENTITY_CONTRACT,
Code::UnsupportedContract,
"identity.contract_version",
);
if !report.refusals.is_empty() {
return Ok(report);
}
let (id, evaluation) = Evaluation::from_arguments(&request.arguments())?;
let route = super::resolve_route(&evaluation.policy.provider, &evaluation.policy.model);
let expected = request_identity(
&evaluation.state,
&evaluation.questions,
&evaluation.policy,
route.as_ref(),
);
let mut check = |matches: bool, code, field: &str| {
if !matches {
report.refusals.push(VerificationRefusal {
code,
field: field.into(),
});
}
};
for (actual, expected, field) in [
(
&receipt.identity.evaluator_instruction_version,
&expected.evaluator_instruction_version,
"identity.evaluator_instruction_version",
),
(
&receipt.identity.output_schema_version,
&expected.output_schema_version,
"identity.output_schema_version",
),
(
&receipt.identity.protocol,
&expected.protocol,
"identity.protocol",
),
] {
check(actual == expected, Code::UnsupportedContract, field);
}
check(
receipt.identity.structured_output_strategy == expected.structured_output_strategy,
Code::UnsupportedContract,
"identity.structured_output_strategy",
);
check(receipt.site_id == id, Code::SiteMismatch, "site_id");
check(
receipt.requested_provider == evaluation.policy.provider,
Code::RouteMismatch,
"requested_provider",
);
check(
receipt.requested_model == evaluation.policy.model,
Code::RouteMismatch,
"requested_model",
);
check(
receipt.identity.input_digest == expected.input_digest,
Code::InputMismatch,
"identity.input_digest",
);
check(
receipt.identity.canonical_input_type == expected.canonical_input_type,
Code::InputMismatch,
"identity.canonical_input_type",
);
check(
receipt.identity.question_set_digest == expected.question_set_digest,
Code::QuestionsMismatch,
"identity.question_set_digest",
);
check(
receipt.identity.policy_digest == expected.policy_digest,
Code::PolicyMismatch,
"identity.policy_digest",
);
check(
receipt.identity.backend_kind == expected.backend_kind,
Code::PolicyMismatch,
"identity.backend_kind",
);
let stable_id = request_id(&id, &expected);
check(
receipt.evaluation_id == stable_id,
Code::IdentityMismatch,
"evaluation_id",
);
report.verified = report.refusals.is_empty();
report.request_identity = Some(expected);
report.stable_request_id = Some(stable_id);
Ok(report)
}