use exo_core::Did;
use serde::{Deserialize, Serialize};
use crate::{
combinator::{CheckpointId, Combinator, CombinatorInput, CombinatorOutput, reduce},
error::GatekeeperError,
kernel::{ActionRequest, AdjudicationContext, Kernel, Verdict},
types::PermissionSet,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HolonState {
Idle,
Executing,
Suspended,
Terminated,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Checkpoint {
pub id: CheckpointId,
pub holon_id: Did,
pub state: HolonState,
pub last_output: Option<CombinatorOutput>,
}
#[derive(Debug, Clone)]
pub struct Holon {
pub id: Did,
pub capabilities: PermissionSet,
pub state: HolonState,
pub combinator_chain: Combinator,
pub last_output: Option<CombinatorOutput>,
}
#[must_use]
pub fn spawn(id: Did, capabilities: PermissionSet, program: Combinator) -> Holon {
Holon {
id,
capabilities,
state: HolonState::Idle,
combinator_chain: program,
last_output: None,
}
}
pub fn step(
holon: &mut Holon,
input: &CombinatorInput,
kernel: &Kernel,
adjudication_context: &AdjudicationContext,
) -> Result<CombinatorOutput, GatekeeperError> {
match holon.state {
HolonState::Terminated => {
return Err(GatekeeperError::HolonError(
"Cannot step a terminated holon".into(),
));
}
HolonState::Suspended => {
return Err(GatekeeperError::HolonError(
"Cannot step a suspended holon — resume first".into(),
));
}
_ => {}
}
let action = ActionRequest {
actor: holon.id.clone(),
action: "holon_step".into(),
required_permissions: holon.capabilities.clone(),
is_self_grant: false,
modifies_kernel: false,
};
match kernel.adjudicate(&action, adjudication_context) {
Verdict::Permitted => {}
Verdict::Denied { violations } => {
holon.state = HolonState::Terminated;
let descs: Vec<String> = violations.iter().map(|v| v.description.clone()).collect();
return Err(GatekeeperError::CapabilityDenied(descs.join("; ")));
}
Verdict::Escalated { reason } => {
holon.state = HolonState::Suspended;
return Err(GatekeeperError::HolonError(format!(
"Step escalated: {}",
reason
)));
}
}
holon.state = HolonState::Executing;
let output = reduce(&holon.combinator_chain, input)?;
holon.last_output = Some(output.clone());
holon.state = HolonState::Idle;
Ok(output)
}
pub fn suspend(holon: &mut Holon) -> Result<Checkpoint, GatekeeperError> {
if holon.state == HolonState::Terminated {
return Err(GatekeeperError::HolonError(
"Cannot suspend a terminated holon".into(),
));
}
holon.state = HolonState::Suspended;
Ok(Checkpoint {
id: CheckpointId(format!("checkpoint-{}", holon.id)),
holon_id: holon.id.clone(),
state: holon.state,
last_output: holon.last_output.clone(),
})
}
pub fn resume(holon: &mut Holon, checkpoint: &Checkpoint) -> Result<(), GatekeeperError> {
if holon.state != HolonState::Suspended {
return Err(GatekeeperError::HolonError(
"Can only resume a suspended holon".into(),
));
}
if checkpoint.holon_id != holon.id {
return Err(GatekeeperError::CheckpointError(
"Checkpoint holon ID does not match".into(),
));
}
holon.last_output.clone_from(&checkpoint.last_output);
holon.state = HolonState::Idle;
Ok(())
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
use crate::{
combinator::{Predicate, TransformFn},
invariants::{
InvariantSet, authority_link_signature_message, provenance_signature_message,
},
types::*,
};
fn did(s: &str) -> Did {
Did::new(s).expect("valid DID")
}
fn signed_link(grantor_str: &str, grantee: &Did) -> AuthorityLink {
let (pk, sk) = exo_core::crypto::generate_keypair();
let grantor = did(grantor_str);
let permissions = PermissionSet::new(vec![Permission::new("read")]);
let mut link = AuthorityLink {
grantor,
grantee: grantee.clone(),
permissions,
signature: Vec::new(),
grantor_public_key: Some(pk.as_bytes().to_vec()),
};
let message = authority_link_signature_message(&link).expect("canonical link payload");
let signature = exo_core::crypto::sign(message.as_bytes(), &sk);
link.signature = signature.to_bytes().to_vec();
link
}
fn signed_provenance(actor: &Did) -> (Provenance, exo_core::PublicKey) {
let (pk, sk) = exo_core::crypto::generate_keypair();
let timestamp = "2025-01-01T00:00:00Z".to_owned();
let action_hash = vec![1];
let mut provenance = Provenance {
actor: actor.clone(),
timestamp,
action_hash,
signature: Vec::new(),
public_key: Some(pk.as_bytes().to_vec()),
voice_kind: None,
independence: None,
review_order: None,
};
let message =
provenance_signature_message(&provenance).expect("canonical provenance payload");
let signature = exo_core::crypto::sign(message.as_bytes(), &sk);
provenance.signature = signature.to_bytes().to_vec();
(provenance, pk)
}
fn test_kernel() -> Kernel {
Kernel::new(b"constitution", InvariantSet::all())
}
fn test_holon() -> Holon {
spawn(
did("did:exo:holon1"),
PermissionSet::new(vec![Permission::new("read")]),
Combinator::Identity,
)
}
fn valid_adj(actor: &Did) -> AdjudicationContext {
let authority_chain = AuthorityChain {
links: vec![signed_link("did:exo:root", actor)],
};
let mut trusted_authority_keys = TrustedAuthorityKeys::default();
for link in &authority_chain.links {
if let Some(public_key) = &link.grantor_public_key {
trusted_authority_keys.insert(link.grantor.clone(), vec![public_key.clone()]);
}
}
let (provenance, provenance_public_key) = signed_provenance(actor);
let mut trusted_provenance_keys = TrustedProvenanceKeys::default();
trusted_provenance_keys.insert(
actor.clone(),
vec![provenance_public_key.as_bytes().to_vec()],
);
AdjudicationContext {
actor_roles: vec![Role {
name: "worker".into(),
branch: GovernmentBranch::Executive,
}],
authority_chain,
consent_records: vec![ConsentRecord {
subject: did("did:exo:owner"),
granted_to: actor.clone(),
scope: "data:read".into(),
active: true,
}],
bailment_state: BailmentState::Active {
bailor: did("did:exo:owner"),
bailee: actor.clone(),
scope: "data:read".into(),
},
human_override_preserved: true,
actor_permissions: PermissionSet::new(vec![Permission::new("read")]),
trusted_authority_keys,
trusted_provenance_keys,
provenance: Some(provenance),
quorum_evidence: None,
active_challenge_reason: None,
}
}
#[test]
fn spawn_creates_idle() {
let h = test_holon();
assert_eq!(h.state, HolonState::Idle);
assert!(h.last_output.is_none());
}
#[test]
fn step_succeeds() {
let kernel = test_kernel();
let mut h = test_holon();
let ctx = valid_adj(&h.id);
let out = step(
&mut h,
&CombinatorInput::new().with("x", "1"),
&kernel,
&ctx,
)
.unwrap();
assert_eq!(out.fields.get("x"), Some(&"1".to_string()));
assert_eq!(h.state, HolonState::Idle);
}
#[test]
fn step_denied_no_consent() {
let kernel = test_kernel();
let mut h = test_holon();
let mut ctx = valid_adj(&h.id);
ctx.bailment_state = BailmentState::None;
assert!(step(&mut h, &CombinatorInput::new(), &kernel, &ctx).is_err());
assert_eq!(h.state, HolonState::Terminated);
}
#[test]
fn step_fails_terminated() {
let kernel = test_kernel();
let mut h = test_holon();
h.state = HolonState::Terminated;
let ctx = valid_adj(&h.id);
assert!(step(&mut h, &CombinatorInput::new(), &kernel, &ctx).is_err());
}
#[test]
fn step_fails_suspended() {
let kernel = test_kernel();
let mut h = test_holon();
h.state = HolonState::Suspended;
let ctx = valid_adj(&h.id);
assert!(step(&mut h, &CombinatorInput::new(), &kernel, &ctx).is_err());
}
#[test]
fn step_enforces_separation_of_powers() {
let kernel = test_kernel();
let mut h = test_holon();
let mut ctx = valid_adj(&h.id);
ctx.actor_roles = vec![
Role {
name: "judge".into(),
branch: GovernmentBranch::Judicial,
},
Role {
name: "senator".into(),
branch: GovernmentBranch::Legislative,
},
];
assert!(step(&mut h, &CombinatorInput::new(), &kernel, &ctx).is_err());
assert_eq!(h.state, HolonState::Terminated);
}
#[test]
fn suspend_and_resume() {
let mut h = test_holon();
let cp = suspend(&mut h).unwrap();
assert_eq!(h.state, HolonState::Suspended);
resume(&mut h, &cp).unwrap();
assert_eq!(h.state, HolonState::Idle);
}
#[test]
fn suspend_fails_terminated() {
let mut h = test_holon();
h.state = HolonState::Terminated;
assert!(suspend(&mut h).is_err());
}
#[test]
fn resume_fails_not_suspended() {
let mut h = test_holon();
let cp = Checkpoint {
id: CheckpointId("t".into()),
holon_id: h.id.clone(),
state: HolonState::Idle,
last_output: None,
};
assert!(resume(&mut h, &cp).is_err());
}
#[test]
fn resume_fails_mismatch() {
let mut h = test_holon();
h.state = HolonState::Suspended;
let cp = Checkpoint {
id: CheckpointId("t".into()),
holon_id: did("did:exo:wrong"),
state: HolonState::Suspended,
last_output: None,
};
assert!(resume(&mut h, &cp).is_err());
}
#[test]
fn step_with_transform() {
let kernel = test_kernel();
let mut h = spawn(
did("did:exo:holon1"),
PermissionSet::new(vec![Permission::new("read")]),
Combinator::Transform(
Box::new(Combinator::Identity),
TransformFn {
name: "e".into(),
output_key: "enriched".into(),
output_value: "true".into(),
},
),
);
let ctx = valid_adj(&h.id);
let out = step(
&mut h,
&CombinatorInput::new().with("d", "r"),
&kernel,
&ctx,
)
.unwrap();
assert_eq!(out.fields.get("enriched"), Some(&"true".to_string()));
}
#[test]
fn full_lifecycle() {
let kernel = test_kernel();
let mut h = test_holon();
let input = CombinatorInput::new().with("x", "1");
let ctx = valid_adj(&h.id);
let _ = step(&mut h, &input, &kernel, &ctx).unwrap();
let cp = suspend(&mut h).unwrap();
resume(&mut h, &cp).unwrap();
let out = step(&mut h, &input, &kernel, &ctx).unwrap();
assert_eq!(out.fields.get("x"), Some(&"1".to_string()));
}
#[test]
fn step_guard_success() {
let kernel = test_kernel();
let mut h = spawn(
did("did:exo:holon1"),
PermissionSet::new(vec![Permission::new("read")]),
Combinator::Guard(
Box::new(Combinator::Identity),
Predicate {
name: "t".into(),
required_key: "token".into(),
expected_value: None,
},
),
);
let ctx = valid_adj(&h.id);
assert!(
step(
&mut h,
&CombinatorInput::new().with("token", "abc"),
&kernel,
&ctx
)
.is_ok()
);
}
#[test]
fn step_guard_failure() {
let kernel = test_kernel();
let mut h = spawn(
did("did:exo:holon1"),
PermissionSet::new(vec![Permission::new("read")]),
Combinator::Guard(
Box::new(Combinator::Identity),
Predicate {
name: "t".into(),
required_key: "token".into(),
expected_value: None,
},
),
);
let ctx = valid_adj(&h.id);
assert!(step(&mut h, &CombinatorInput::new(), &kernel, &ctx).is_err());
}
#[test]
fn step_enforces_combinator_timeout_budget() {
let kernel = test_kernel();
let mut h = spawn(
did("did:exo:timeout-holon"),
PermissionSet::new(vec![Permission::new("read")]),
Combinator::Timeout(
Box::new(Combinator::Sequence(vec![
Combinator::Identity,
Combinator::Identity,
])),
crate::combinator::Duration(2),
),
);
let ctx = valid_adj(&h.id);
let err = step(&mut h, &CombinatorInput::new(), &kernel, &ctx)
.expect_err("over-budget holon combinator must fail closed");
assert!(
err.to_string().contains("timeout budget exhausted"),
"unexpected holon timeout error: {err}"
);
}
#[test]
fn holon_isolation_termination_does_not_affect_sibling() {
let kernel = test_kernel();
let mut holon_a = spawn(
did("did:exo:holon-a"),
PermissionSet::new(vec![Permission::new("read")]),
Combinator::Identity,
);
let mut holon_b = spawn(
did("did:exo:holon-b"),
PermissionSet::new(vec![Permission::new("read")]),
Combinator::Identity,
);
let mut bad_ctx = valid_adj(&holon_a.id);
bad_ctx.bailment_state = BailmentState::None;
assert!(step(&mut holon_a, &CombinatorInput::new(), &kernel, &bad_ctx).is_err());
assert_eq!(holon_a.state, HolonState::Terminated);
assert_eq!(holon_b.state, HolonState::Idle);
let ctx_b = valid_adj(&holon_b.id);
let out = step(
&mut holon_b,
&CombinatorInput::new().with("key", "val"),
&kernel,
&ctx_b,
)
.unwrap();
assert_eq!(out.fields.get("key"), Some(&"val".to_string()));
}
#[test]
fn holon_isolation_cross_checkpoint_resume_fails() {
let mut holon_a = spawn(
did("did:exo:holon-a"),
PermissionSet::new(vec![Permission::new("read")]),
Combinator::Identity,
);
let mut holon_b = spawn(
did("did:exo:holon-b"),
PermissionSet::new(vec![Permission::new("read")]),
Combinator::Identity,
);
let _cp_a = suspend(&mut holon_a).unwrap();
let cp_b = suspend(&mut holon_b).unwrap();
assert!(resume(&mut holon_a, &cp_b).is_err());
assert_eq!(holon_a.state, HolonState::Suspended);
}
#[test]
fn holon_lifecycle_create_adjudicate_terminate() {
let kernel = test_kernel();
let mut h = spawn(
did("did:exo:lifecycle-holon"),
PermissionSet::new(vec![Permission::new("read")]),
Combinator::Identity,
);
assert_eq!(h.state, HolonState::Idle);
assert!(h.last_output.is_none());
let ctx = valid_adj(&h.id);
let out = step(
&mut h,
&CombinatorInput::new().with("data", "payload"),
&kernel,
&ctx,
)
.unwrap();
assert_eq!(out.fields.get("data"), Some(&"payload".to_string()));
assert_eq!(h.state, HolonState::Idle);
assert!(h.last_output.is_some());
let mut denied_ctx = valid_adj(&h.id);
denied_ctx.bailment_state = BailmentState::None;
assert!(step(&mut h, &CombinatorInput::new(), &kernel, &denied_ctx).is_err());
assert_eq!(h.state, HolonState::Terminated);
assert!(step(&mut h, &CombinatorInput::new(), &kernel, &ctx).is_err());
assert!(suspend(&mut h).is_err());
}
}