use crate::mode::util::{
check_commitment_authority, enforce_commitment_policy, is_declared_participant,
validate_commitment_payload_for_session,
};
use crate::mode::{Mode, ModeResponse};
use macp_core::error::MacpError;
use macp_core::session::Session;
use macp_pb::decision_pb::{EvaluationPayload, ObjectionPayload, ProposalPayload, VotePayload};
use macp_pb::pb::Envelope;
use prost::Message;
use std::collections::BTreeMap;
pub use macp_core::decision::{
DecisionPhase, DecisionState, Evaluation, Objection, Proposal, Vote,
};
pub struct DecisionMode {
evaluator: std::sync::Arc<dyn macp_core::policy::PolicyEvaluator>,
}
impl DecisionMode {
pub fn new(evaluator: std::sync::Arc<dyn macp_core::policy::PolicyEvaluator>) -> Self {
Self { evaluator }
}
fn default_state() -> DecisionState {
DecisionState {
proposals: BTreeMap::new(),
evaluations: Vec::new(),
objections: Vec::new(),
votes: BTreeMap::new(),
phase: DecisionPhase::Proposal,
}
}
fn encode_state(state: &DecisionState) -> Vec<u8> {
crate::mode::util::encode_mode_state(state)
}
fn decode_state(data: &[u8]) -> Result<DecisionState, MacpError> {
crate::mode::util::decode_mode_state(data)
}
fn ensure_not_committed(state: &DecisionState) -> Result<(), MacpError> {
if state.phase == DecisionPhase::Committed {
Err(MacpError::SessionNotOpen)
} else {
Ok(())
}
}
fn ensure_known_proposal(state: &DecisionState, proposal_id: &str) -> Result<(), MacpError> {
if !state.proposals.contains_key(proposal_id) {
return Err(MacpError::InvalidPayload);
}
Ok(())
}
fn ensure_can_propose(state: &DecisionState) -> Result<(), MacpError> {
Self::ensure_not_committed(state)?;
if state.phase == DecisionPhase::Voting {
return Err(MacpError::InvalidPayload);
}
Ok(())
}
fn ensure_can_deliberate(state: &DecisionState) -> Result<(), MacpError> {
Self::ensure_not_committed(state)?;
if state.phase != DecisionPhase::Evaluation {
return Err(MacpError::InvalidPayload);
}
Ok(())
}
fn ensure_can_vote(state: &DecisionState) -> Result<(), MacpError> {
Self::ensure_not_committed(state)?;
if matches!(
state.phase,
DecisionPhase::Proposal | DecisionPhase::Committed
) {
return Err(MacpError::InvalidPayload);
}
Ok(())
}
fn commitment_ready(state: &DecisionState) -> bool {
!state.proposals.is_empty()
}
}
impl Mode for DecisionMode {
fn authorize_sender(&self, session: &Session, env: &Envelope) -> Result<(), MacpError> {
match env.message_type.as_str() {
"Commitment" => check_commitment_authority(session, &env.sender),
"Proposal" | "Evaluation" | "Objection" | "Vote"
if is_declared_participant(&session.participants, &env.sender) =>
{
Ok(())
}
_ => Err(MacpError::Forbidden),
}
}
fn on_session_start(
&self,
session: &Session,
_env: &Envelope,
) -> Result<ModeResponse, MacpError> {
if session.participants.is_empty() {
return Err(MacpError::InvalidPayload);
}
Ok(ModeResponse::PersistState(Self::encode_state(
&Self::default_state(),
)))
}
fn on_message(&self, session: &Session, env: &Envelope) -> Result<ModeResponse, MacpError> {
let mut state = if session.mode_state.is_empty() {
Self::default_state()
} else {
Self::decode_state(&session.mode_state)?
};
Self::ensure_not_committed(&state)?;
match env.message_type.as_str() {
"Proposal" => {
Self::ensure_can_propose(&state)?;
let payload = ProposalPayload::decode(&*env.payload)
.map_err(|_| MacpError::InvalidPayload)?;
if payload.proposal_id.trim().is_empty()
|| payload.option.trim().is_empty()
|| state.proposals.contains_key(&payload.proposal_id)
{
return Err(MacpError::InvalidPayload);
}
state.proposals.insert(
payload.proposal_id.clone(),
Proposal {
proposal_id: payload.proposal_id,
option: payload.option,
rationale: payload.rationale,
sender: env.sender.clone(),
},
);
state.phase = DecisionPhase::Evaluation;
Ok(ModeResponse::PersistState(Self::encode_state(&state)))
}
"Evaluation" => {
let payload = EvaluationPayload::decode(&*env.payload)
.map_err(|_| MacpError::InvalidPayload)?;
match payload.recommendation.to_uppercase().as_str() {
"APPROVE" | "REVIEW" | "BLOCK" | "REJECT" => {}
_ => return Err(MacpError::InvalidPayload),
}
if payload.confidence < 0.0 || payload.confidence > 1.0 {
return Err(MacpError::InvalidPayload);
}
Self::ensure_can_deliberate(&state)?;
Self::ensure_known_proposal(&state, &payload.proposal_id)?;
let normalized_recommendation = payload.recommendation.to_uppercase();
state.evaluations.push(Evaluation {
proposal_id: payload.proposal_id,
recommendation: normalized_recommendation,
confidence: payload.confidence,
reason: payload.reason,
sender: env.sender.clone(),
});
Ok(ModeResponse::PersistState(Self::encode_state(&state)))
}
"Objection" => {
let payload = ObjectionPayload::decode(&*env.payload)
.map_err(|_| MacpError::InvalidPayload)?;
let severity = if payload.severity.is_empty() {
"medium".into()
} else {
match payload.severity.to_lowercase().as_str() {
"critical" | "high" | "medium" | "low" => payload.severity.to_lowercase(),
_ => return Err(MacpError::InvalidPayload),
}
};
Self::ensure_can_deliberate(&state)?;
Self::ensure_known_proposal(&state, &payload.proposal_id)?;
state.objections.push(Objection {
proposal_id: payload.proposal_id,
reason: payload.reason,
severity,
sender: env.sender.clone(),
});
Ok(ModeResponse::PersistState(Self::encode_state(&state)))
}
"Vote" => {
let payload =
VotePayload::decode(&*env.payload).map_err(|_| MacpError::InvalidPayload)?;
let normalized_vote = payload.vote.to_uppercase();
match normalized_vote.as_str() {
"APPROVE" | "REJECT" | "ABSTAIN" => {}
_ => return Err(MacpError::InvalidPayload),
}
Self::ensure_can_vote(&state)?;
Self::ensure_known_proposal(&state, &payload.proposal_id)?;
let proposal_votes = state.votes.entry(payload.proposal_id.clone()).or_default();
if proposal_votes.contains_key(&env.sender) {
return Err(MacpError::InvalidPayload);
}
proposal_votes.insert(
env.sender.clone(),
Vote {
proposal_id: payload.proposal_id,
vote: normalized_vote,
reason: payload.reason,
sender: env.sender.clone(),
},
);
state.phase = DecisionPhase::Voting;
Ok(ModeResponse::PersistState(Self::encode_state(&state)))
}
"Commitment" => {
let commitment = validate_commitment_payload_for_session(session, &env.payload)?;
if !Self::commitment_ready(&state) {
return Err(MacpError::InvalidPayload);
}
enforce_commitment_policy(
session,
macp_core::policy::CommitmentMode::Decision { state: &state },
commitment.outcome_positive,
&*self.evaluator,
)?;
state.phase = DecisionPhase::Committed;
Ok(ModeResponse::PersistAndResolve {
state: Self::encode_state(&state),
resolution: env.payload.clone(),
})
}
_ => Err(MacpError::InvalidPayload),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use macp_pb::pb::CommitmentPayload;
fn test_session() -> Session {
Session::builder("s1", "macp.mode.decision.v1", "agent://orchestrator")
.ttl_ms(60_000)
.participants(vec![
"agent://orchestrator".into(),
"agent://fraud".into(),
"agent://growth".into(),
])
.mode_version("1.0.0")
.configuration_version("cfg-1")
.policy_version("policy-1")
.build()
}
fn env(sender: &str, message_type: &str, payload: Vec<u8>) -> Envelope {
Envelope {
macp_version: "1.0".into(),
mode: "macp.mode.decision.v1".into(),
message_type: message_type.into(),
message_id: format!("{}-{}", sender, message_type),
session_id: "s1".into(),
sender: sender.into(),
timestamp_unix_ms: 0,
payload,
}
}
fn proposal(id: &str) -> Vec<u8> {
ProposalPayload {
proposal_id: id.into(),
option: format!("option-{id}"),
rationale: "because".into(),
supporting_data: vec![],
}
.encode_to_vec()
}
fn vote(id: &str, value: &str) -> Vec<u8> {
VotePayload {
proposal_id: id.into(),
vote: value.into(),
reason: String::new(),
}
.encode_to_vec()
}
fn evaluation(proposal_id: &str) -> Vec<u8> {
EvaluationPayload {
proposal_id: proposal_id.into(),
recommendation: "APPROVE".into(),
confidence: 0.9,
reason: "good".into(),
}
.encode_to_vec()
}
fn objection(proposal_id: &str) -> Vec<u8> {
ObjectionPayload {
proposal_id: proposal_id.into(),
reason: "risky".into(),
severity: "high".into(),
}
.encode_to_vec()
}
fn commitment(session: &Session) -> Vec<u8> {
CommitmentPayload {
commitment_id: "c1".into(),
action: "decision.selected".into(),
authority_scope: "payments".into(),
reason: "bound".into(),
mode_version: session.mode_version.clone(),
policy_version: session.policy_version.clone(),
configuration_version: session.configuration_version.clone(),
outcome_positive: true,
supersedes: None,
}
.encode_to_vec()
}
fn apply(session: &mut Session, response: ModeResponse) {
match response {
ModeResponse::PersistState(data) => session.mode_state = data,
ModeResponse::PersistAndResolve { state, .. } => session.mode_state = state,
_ => {}
}
}
fn decode(session: &Session) -> DecisionState {
serde_json::from_slice(&session.mode_state).unwrap()
}
#[test]
fn session_start_requires_declared_participants() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
session.participants.clear();
assert_eq!(
mode.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![])
)
.unwrap_err()
.to_string(),
"InvalidPayload"
);
}
#[test]
fn initiator_not_in_participants_cannot_propose() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
session.participants.retain(|p| p != "agent://orchestrator");
let err = mode
.authorize_sender(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap_err();
assert_eq!(err.to_string(), "Forbidden");
}
#[test]
fn vote_with_invalid_value_rejected() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
session
.participants
.push("agent://orchestrator".to_string());
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
let bad_vote = VotePayload {
proposal_id: "p1".into(),
vote: "maybe".into(),
reason: String::new(),
}
.encode_to_vec();
let err = mode
.on_message(&session, &env("agent://fraud", "Vote", bad_vote))
.unwrap_err();
assert_eq!(err.to_string(), "InvalidPayload");
}
#[test]
fn abstain_vote_accepted() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
session
.participants
.push("agent://orchestrator".to_string());
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
mode.on_message(
&session,
&env("agent://fraud", "Vote", vote("p1", "abstain")),
)
.unwrap();
}
#[test]
fn evaluation_with_invalid_recommendation_rejected() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
session
.participants
.push("agent://orchestrator".to_string());
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
let bad_eval = EvaluationPayload {
proposal_id: "p1".into(),
recommendation: "meh".into(),
confidence: 0.5,
reason: "unclear".into(),
}
.encode_to_vec();
let err = mode
.on_message(&session, &env("agent://fraud", "Evaluation", bad_eval))
.unwrap_err();
assert_eq!(err.to_string(), "InvalidPayload");
}
#[test]
fn duplicate_proposal_id_is_rejected() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
assert_eq!(
mode.on_message(&session, &env("agent://fraud", "Proposal", proposal("p1")))
.unwrap_err()
.to_string(),
"InvalidPayload"
);
}
#[test]
fn vote_is_scoped_per_proposal() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(&session, &env("agent://fraud", "Proposal", proposal("p2")))
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://fraud", "Vote", vote("p1", "approve")),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://fraud", "Vote", vote("p2", "approve")),
)
.unwrap();
apply(&mut session, resp);
assert_eq!(
mode.on_message(
&session,
&env("agent://fraud", "Vote", vote("p1", "reject"))
)
.unwrap_err()
.to_string(),
"InvalidPayload"
);
}
#[test]
fn evaluation_before_any_proposal_rejected() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
assert_eq!(
mode.on_message(
&session,
&env("agent://fraud", "Evaluation", evaluation("p1"))
)
.unwrap_err()
.to_string(),
"InvalidPayload"
);
}
#[test]
fn objection_before_any_proposal_rejected() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
assert_eq!(
mode.on_message(
&session,
&env("agent://fraud", "Objection", objection("p1"))
)
.unwrap_err()
.to_string(),
"InvalidPayload"
);
}
#[test]
fn vote_before_any_proposal_rejected() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
assert_eq!(
mode.on_message(
&session,
&env("agent://fraud", "Vote", vote("p1", "approve"))
)
.unwrap_err()
.to_string(),
"InvalidPayload"
);
}
#[test]
fn proposal_after_voting_rejected() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://fraud", "Vote", vote("p1", "approve")),
)
.unwrap();
apply(&mut session, resp);
assert_eq!(
mode.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p2"))
)
.unwrap_err()
.to_string(),
"InvalidPayload"
);
}
#[test]
fn evaluation_after_voting_rejected() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://fraud", "Vote", vote("p1", "approve")),
)
.unwrap();
apply(&mut session, resp);
assert_eq!(
mode.on_message(
&session,
&env("agent://growth", "Evaluation", evaluation("p1"))
)
.unwrap_err()
.to_string(),
"InvalidPayload"
);
}
#[test]
fn objection_after_voting_rejected() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://fraud", "Vote", vote("p1", "approve")),
)
.unwrap();
apply(&mut session, resp);
assert_eq!(
mode.on_message(
&session,
&env("agent://growth", "Objection", objection("p1"))
)
.unwrap_err()
.to_string(),
"InvalidPayload"
);
}
#[test]
fn commitment_from_non_initiator_rejected() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
assert_eq!(
mode.authorize_sender(
&session,
&env("agent://fraud", "Commitment", commitment(&session))
)
.unwrap_err()
.to_string(),
"Forbidden"
);
}
#[test]
fn empty_proposal_id_rejected() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let empty_proposal = ProposalPayload {
proposal_id: "".into(),
option: "option".into(),
rationale: "because".into(),
supporting_data: vec![],
}
.encode_to_vec();
assert_eq!(
mode.on_message(
&session,
&env("agent://orchestrator", "Proposal", empty_proposal)
)
.unwrap_err()
.to_string(),
"InvalidPayload"
);
}
#[test]
fn malformed_vote_payload_rejected() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
assert_eq!(
mode.on_message(&session, &env("agent://fraud", "Vote", vec![0xff, 0x00]))
.unwrap_err()
.to_string(),
"InvalidPayload"
);
}
#[test]
fn phase_advances_from_proposal_to_voting_to_committed() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
assert_eq!(decode(&session).phase, DecisionPhase::Proposal);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
assert_eq!(decode(&session).phase, DecisionPhase::Evaluation);
let resp = mode
.on_message(
&session,
&env("agent://fraud", "Vote", vote("p1", "approve")),
)
.unwrap();
apply(&mut session, resp);
assert_eq!(decode(&session).phase, DecisionPhase::Voting);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Commitment", commitment(&session)),
)
.unwrap();
apply(&mut session, resp);
assert_eq!(decode(&session).phase, DecisionPhase::Committed);
}
#[test]
fn commitment_versions_must_match_session_bindings() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
let mut bad = CommitmentPayload {
commitment_id: "c1".into(),
action: "decision.selected".into(),
authority_scope: "payments".into(),
reason: "bound".into(),
mode_version: "wrong".into(),
policy_version: session.policy_version.clone(),
configuration_version: session.configuration_version.clone(),
outcome_positive: true,
supersedes: None,
}
.encode_to_vec();
assert_eq!(
mode.on_message(
&session,
&env("agent://orchestrator", "Commitment", bad.clone())
)
.unwrap_err()
.to_string(),
"InvalidPayload"
);
bad = commitment(&session);
mode.on_message(&session, &env("agent://orchestrator", "Commitment", bad))
.unwrap();
}
#[test]
fn negative_outcome_commitment_succeeds() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://fraud", "Vote", vote("p1", "reject")),
)
.unwrap();
apply(&mut session, resp);
let negative_commitment = CommitmentPayload {
commitment_id: "c1".into(),
action: "decision.rejected".into(),
authority_scope: "payments".into(),
reason: "proposal rejected by voters".into(),
mode_version: session.mode_version.clone(),
policy_version: session.policy_version.clone(),
configuration_version: session.configuration_version.clone(),
outcome_positive: false,
supersedes: None,
}
.encode_to_vec();
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Commitment", negative_commitment),
)
.unwrap();
assert!(matches!(resp, ModeResponse::PersistAndResolve { .. }));
apply(&mut session, resp);
assert_eq!(decode(&session).phase, DecisionPhase::Committed);
}
#[test]
fn policy_bound_reject_majority_finalizes_decline() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
session.policy_definition = Some(macp_core::policy::PolicyDefinition {
policy_id: "majority".into(),
mode: "macp.mode.decision.v1".into(),
description: "majority".into(),
rules: serde_json::json!({
"voting": { "algorithm": "majority", "threshold": 0.5 }
}),
schema_version: 1,
});
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://fraud", "Vote", vote("p1", "reject")),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://growth", "Vote", vote("p1", "reject")),
)
.unwrap();
apply(&mut session, resp);
let positive = CommitmentPayload {
commitment_id: "c1".into(),
action: "decision.selected".into(),
authority_scope: "payments".into(),
reason: "approved".into(),
mode_version: session.mode_version.clone(),
policy_version: session.policy_version.clone(),
configuration_version: session.configuration_version.clone(),
outcome_positive: true,
supersedes: None,
}
.encode_to_vec();
let err = mode
.on_message(
&session,
&env("agent://orchestrator", "Commitment", positive),
)
.unwrap_err();
assert_eq!(err.to_string(), "PolicyDenied");
let decline = CommitmentPayload {
commitment_id: "c2".into(),
action: "decision.rejected".into(),
authority_scope: "payments".into(),
reason: "rejected by majority".into(),
mode_version: session.mode_version.clone(),
policy_version: session.policy_version.clone(),
configuration_version: session.configuration_version.clone(),
outcome_positive: false,
supersedes: None,
}
.encode_to_vec();
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Commitment", decline),
)
.unwrap();
let resolution = match &resp {
ModeResponse::PersistAndResolve { resolution, .. } => resolution.clone(),
other => panic!("expected PersistAndResolve, got {other:?}"),
};
let resolved = CommitmentPayload::decode(resolution.as_slice()).unwrap();
assert!(!resolved.outcome_positive);
assert_eq!(resolved.action, "decision.rejected");
apply(&mut session, resp);
assert_eq!(decode(&session).phase, DecisionPhase::Committed);
}
#[test]
fn policy_denies_commitment_when_vote_threshold_not_met() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
session.policy_definition = Some(macp_core::policy::PolicyDefinition {
policy_id: "test-strict".into(),
mode: "macp.mode.decision.v1".into(),
description: "strict".into(),
rules: serde_json::json!({
"voting": { "algorithm": "unanimous" }
}),
schema_version: 1,
});
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://fraud", "Vote", vote("p1", "approve")),
)
.unwrap();
apply(&mut session, resp);
let err = mode
.on_message(
&session,
&env("agent://orchestrator", "Commitment", commitment(&session)),
)
.unwrap_err();
assert_eq!(err.to_string(), "PolicyDenied");
}
struct DenyAllEvaluator;
impl macp_core::policy::PolicyEvaluator for DenyAllEvaluator {
fn evaluate_commitment(
&self,
ctx: &macp_core::policy::CommitmentContext<'_>,
) -> macp_core::policy::PolicyDecision {
match ctx.mode {
macp_core::policy::CommitmentMode::Decision { .. } => {
macp_core::policy::PolicyDecision::Deny {
reasons: vec!["custom evaluator denies all".into()],
}
}
_ => macp_core::policy::PolicyDecision::Allow { reasons: vec![] },
}
}
}
fn session_ready_for_commitment(mode: &DecisionMode) -> (Session, Envelope) {
let mut session = test_session();
session.policy_definition = Some(macp_core::policy::PolicyDefinition {
policy_id: "permissive".into(),
mode: "macp.mode.decision.v1".into(),
description: "no extra constraints".into(),
rules: serde_json::json!({}),
schema_version: 1,
});
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
let commit = env("agent://orchestrator", "Commitment", commitment(&session));
(session, commit)
}
#[test]
fn injected_evaluator_governs_commitment_outcome() {
let default_mode =
DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let (session, commit) = session_ready_for_commitment(&default_mode);
let resp = default_mode.on_message(&session, &commit).unwrap();
assert!(matches!(resp, ModeResponse::PersistAndResolve { .. }));
let custom_mode = DecisionMode::new(std::sync::Arc::new(DenyAllEvaluator));
let (session, commit) = session_ready_for_commitment(&custom_mode);
let err = custom_mode.on_message(&session, &commit).unwrap_err();
assert_eq!(err.to_string(), "PolicyDenied");
}
#[test]
fn evaluation_confidence_out_of_bounds_rejected() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
let bad_eval = EvaluationPayload {
proposal_id: "p1".into(),
recommendation: "APPROVE".into(),
confidence: 1.5,
reason: "too confident".into(),
}
.encode_to_vec();
assert_eq!(
mode.on_message(&session, &env("agent://fraud", "Evaluation", bad_eval))
.unwrap_err()
.to_string(),
"InvalidPayload"
);
}
#[test]
fn evaluation_confidence_negative_rejected() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
let bad_eval = EvaluationPayload {
proposal_id: "p1".into(),
recommendation: "APPROVE".into(),
confidence: -0.1,
reason: "negative".into(),
}
.encode_to_vec();
assert_eq!(
mode.on_message(&session, &env("agent://fraud", "Evaluation", bad_eval))
.unwrap_err()
.to_string(),
"InvalidPayload"
);
}
#[test]
fn evaluation_confidence_boundary_accepted() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
let eval_zero = EvaluationPayload {
proposal_id: "p1".into(),
recommendation: "APPROVE".into(),
confidence: 0.0,
reason: "zero".into(),
}
.encode_to_vec();
let resp = mode
.on_message(&session, &env("agent://fraud", "Evaluation", eval_zero))
.unwrap();
apply(&mut session, resp);
let eval_one = EvaluationPayload {
proposal_id: "p1".into(),
recommendation: "REVIEW".into(),
confidence: 1.0,
reason: "one".into(),
}
.encode_to_vec();
mode.on_message(&session, &env("agent://growth", "Evaluation", eval_one))
.unwrap();
}
#[test]
fn objection_invalid_severity_rejected() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
let bad_objection = ObjectionPayload {
proposal_id: "p1".into(),
reason: "bad".into(),
severity: "urgent".into(),
}
.encode_to_vec();
assert_eq!(
mode.on_message(&session, &env("agent://fraud", "Objection", bad_objection))
.unwrap_err()
.to_string(),
"InvalidPayload"
);
}
#[test]
fn objection_valid_severities_accepted() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
for severity in &["critical", "high", "medium", "low"] {
let obj = ObjectionPayload {
proposal_id: "p1".into(),
reason: "reason".into(),
severity: severity.to_string(),
}
.encode_to_vec();
let resp = mode
.on_message(&session, &env("agent://fraud", "Objection", obj))
.unwrap();
apply(&mut session, resp);
}
}
#[test]
fn objection_severity_case_normalized() {
let mode = DecisionMode::new(std::sync::Arc::new(macp_policy::DefaultPolicyEvaluator));
let mut session = test_session();
let resp = mode
.on_session_start(
&session,
&env("agent://orchestrator", "SessionStart", vec![]),
)
.unwrap();
apply(&mut session, resp);
let resp = mode
.on_message(
&session,
&env("agent://orchestrator", "Proposal", proposal("p1")),
)
.unwrap();
apply(&mut session, resp);
let obj = ObjectionPayload {
proposal_id: "p1".into(),
reason: "reason".into(),
severity: "CRITICAL".into(),
}
.encode_to_vec();
let resp = mode
.on_message(&session, &env("agent://fraud", "Objection", obj))
.unwrap();
apply(&mut session, resp);
let state = decode(&session);
assert_eq!(state.objections[0].severity, "critical");
}
}