feat(agorapura): identidad humana federada — core + grafo de confianza

agorapura-core: identidades fractales (persona/comunidad/alianza/
institución) sobre claves ed25519, Claims sujeto-predicado-valor y
Attestations firmadas y autoverificables (la prueba viaja con el
dato). agorapura-graph: TrustGraph guarda sólo atestaciones con firma
válida; corroboration() devuelve evidencia cruda y TrustPolicy —un
umbral negociado, no una verdad del sistema— la traduce a sí/no.

22 tests. Cero red, cero estado global, #![forbid(unsafe_code)].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-20 16:38:20 +00:00
parent ad9781c2ee
commit c1c136954e
10 changed files with 847 additions and 0 deletions
@@ -0,0 +1,306 @@
//! `agorapura-graph` — la red de confianza del ágora.
//!
//! Acumula [`Attestation`]s **verificadas** (una atestación con firma
//! rota nunca entra) y responde preguntas de corroboración: *¿quién
//! respalda este claim?*
//!
//! El grafo deliberadamente **no** emite un veredicto. La verdad del
//! ágora no es absoluta: depende de cuánto peso le dé a cada atestador
//! quien la consulta. Por eso [`TrustGraph::corroboration`] devuelve la
//! evidencia cruda y [`TrustPolicy`] —un umbral *negociado*— la traduce
//! a un sí/no. Dos consumidores con políticas distintas pueden mirar la
//! misma red y discrepar legítimamente.
#![forbid(unsafe_code)]
use std::collections::HashMap;
use agorapura_core::{AgoraError, Attestation, Identity, IdentityId};
use serde::{Deserialize, Serialize};
/// Evidencia acumulada a favor de un claim concreto.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Corroboration {
/// Atestadores distintos que respaldan exactamente este claim.
pub attesters: Vec<IdentityId>,
/// El propio sujeto figura entre los atestadores.
pub self_attested: bool,
}
impl Corroboration {
/// `true` si nadie respalda el claim.
pub fn is_empty(&self) -> bool {
self.attesters.is_empty()
}
/// Atestadores totales (incluido el sujeto si se auto-atestó).
pub fn total(&self) -> usize {
self.attesters.len()
}
/// Atestadores que no son el sujeto — la evidencia independiente.
pub fn third_party(&self) -> usize {
self.total() - usize::from(self.self_attested)
}
}
/// Umbral *negociado* de aceptación de un claim. No es una verdad del
/// sistema: cada consumidor adopta el suyo según lo que pacte.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrustPolicy {
/// Atestadores terceros distintos exigidos como mínimo.
pub min_third_party: usize,
/// Si la auto-atestación del sujeto cuenta como respaldo válido.
pub accept_self: bool,
}
impl TrustPolicy {
/// Política estricta: al menos `n` terceros, la auto-atestación no
/// suma.
pub fn strict(min_third_party: usize) -> Self {
Self { min_third_party, accept_self: false }
}
/// `true` si la evidencia satisface la política.
pub fn accepts(&self, c: &Corroboration) -> bool {
if c.third_party() >= self.min_third_party {
return true;
}
self.accept_self && c.self_attested && self.min_third_party == 0
}
}
impl Default for TrustPolicy {
/// Por defecto: un tercero basta, la auto-atestación no.
fn default() -> Self {
Self::strict(1)
}
}
/// La red de confianza: identidades conocidas + atestaciones verificadas.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TrustGraph {
identities: HashMap<IdentityId, Identity>,
/// Atestaciones verificadas, en orden de inserción.
attestations: Vec<Attestation>,
}
impl TrustGraph {
pub fn new() -> Self {
Self::default()
}
/// Registra (o actualiza) una identidad conocida.
pub fn register(&mut self, identity: Identity) {
self.identities.insert(identity.id(), identity);
}
/// Identidad registrada con ese id, si la hay.
pub fn identity(&self, id: IdentityId) -> Option<&Identity> {
self.identities.get(&id)
}
/// Cantidad de identidades registradas.
pub fn identity_count(&self) -> usize {
self.identities.len()
}
/// Cantidad de atestaciones verificadas almacenadas.
pub fn attestation_count(&self) -> usize {
self.attestations.len()
}
/// Verifica una atestación y, si es válida y no es duplicada exacta,
/// la incorpora. Una firma rota se rechaza con error — la red sólo
/// guarda evidencia comprobable.
pub fn add_attestation(&mut self, att: Attestation) -> Result<(), AgoraError> {
att.verify()?;
if !self.attestations.contains(&att) {
self.attestations.push(att);
}
Ok(())
}
/// Atestaciones cuyo claim trata sobre `subject`.
pub fn attestations_about(&self, subject: IdentityId) -> Vec<&Attestation> {
self.attestations
.iter()
.filter(|a| a.claim.subject == subject)
.collect()
}
/// Atestaciones emitidas por `attester`.
pub fn attestations_by(&self, attester: IdentityId) -> Vec<&Attestation> {
self.attestations
.iter()
.filter(|a| a.attester == attester)
.collect()
}
/// Atestaciones que respaldan exactamente el claim
/// `subject · predicate = value` (la marca de tiempo no importa).
pub fn evidence_for(
&self,
subject: IdentityId,
predicate: &str,
value: &str,
) -> Vec<&Attestation> {
self.attestations
.iter()
.filter(|a| {
a.claim.subject == subject
&& a.claim.predicate == predicate
&& a.claim.value == value
})
.collect()
}
/// Resume la corroboración de un claim: atestadores distintos y si
/// el sujeto se lo auto-atestó. El veredicto lo pone una
/// [`TrustPolicy`].
pub fn corroboration(
&self,
subject: IdentityId,
predicate: &str,
value: &str,
) -> Corroboration {
let mut attesters: Vec<IdentityId> = Vec::new();
let mut self_attested = false;
for att in self.evidence_for(subject, predicate, value) {
if att.is_self_attested() {
self_attested = true;
}
if !attesters.contains(&att.attester) {
attesters.push(att.attester);
}
}
Corroboration { attesters, self_attested }
}
/// Atajo: `true` si la `policy` acepta el claim dada la evidencia.
pub fn is_accepted(
&self,
subject: IdentityId,
predicate: &str,
value: &str,
policy: &TrustPolicy,
) -> bool {
policy.accepts(&self.corroboration(subject, predicate, value))
}
}
#[cfg(test)]
mod tests {
use super::*;
use agorapura_core::{Attestation, Claim, IdentityKind, Keypair};
/// Mundo de prueba: Yumaira (persona) + tres atestadores.
fn actors() -> (Keypair, Keypair, Keypair, Keypair) {
(
Keypair::from_seed([20; 32]), // yumaira
Keypair::from_seed([10; 32]), // venezuela (institución)
Keypair::from_seed([30; 32]), // comunidad
Keypair::from_seed([40; 32]), // vecina
)
}
fn attest(by: &Keypair, subject: IdentityId, pred: &str, val: &str) -> Attestation {
Attestation::create(by, Claim::new(subject, pred, val, 1_700_000_000))
}
#[test]
fn rejects_attestation_with_broken_signature() {
let (yumaira, venezuela, ..) = actors();
let mut att = attest(&venezuela, yumaira.identity_id(), "nacionalidad", "venezolana");
att.claim.value = "falsa".into(); // rompe la firma
let mut g = TrustGraph::new();
assert!(g.add_attestation(att).is_err());
assert_eq!(g.attestation_count(), 0);
}
#[test]
fn stores_and_queries_verified_attestations() {
let (yumaira, venezuela, comunidad, _) = actors();
let mut g = TrustGraph::new();
g.register(venezuela.identity(IdentityKind::Institution, "Venezuela"));
g.add_attestation(attest(&venezuela, yumaira.identity_id(), "nacionalidad", "venezolana"))
.unwrap();
g.add_attestation(attest(&comunidad, yumaira.identity_id(), "miembro-de", "Valle"))
.unwrap();
assert_eq!(g.attestations_about(yumaira.identity_id()).len(), 2);
assert_eq!(g.attestations_by(venezuela.identity_id()).len(), 1);
assert_eq!(g.identity_count(), 1);
}
#[test]
fn duplicate_attestation_is_ignored() {
let (yumaira, venezuela, ..) = actors();
let att = attest(&venezuela, yumaira.identity_id(), "nacionalidad", "venezolana");
let mut g = TrustGraph::new();
g.add_attestation(att.clone()).unwrap();
g.add_attestation(att).unwrap();
assert_eq!(g.attestation_count(), 1);
}
#[test]
fn corroboration_counts_distinct_attesters() {
let (yumaira, venezuela, comunidad, vecina) = actors();
let mut g = TrustGraph::new();
for who in [&venezuela, &comunidad, &vecina] {
g.add_attestation(attest(who, yumaira.identity_id(), "nacionalidad", "venezolana"))
.unwrap();
}
let c = g.corroboration(yumaira.identity_id(), "nacionalidad", "venezolana");
assert_eq!(c.total(), 3);
assert_eq!(c.third_party(), 3);
assert!(!c.self_attested);
}
#[test]
fn self_attestation_is_distinguished_from_third_party() {
let (yumaira, venezuela, ..) = actors();
let mut g = TrustGraph::new();
g.add_attestation(attest(&yumaira, yumaira.identity_id(), "habilidad", "soldadura"))
.unwrap();
g.add_attestation(attest(&venezuela, yumaira.identity_id(), "habilidad", "soldadura"))
.unwrap();
let c = g.corroboration(yumaira.identity_id(), "habilidad", "soldadura");
assert_eq!(c.total(), 2);
assert_eq!(c.third_party(), 1);
assert!(c.self_attested);
}
#[test]
fn negotiated_policy_decides_acceptance() {
let (yumaira, venezuela, comunidad, _) = actors();
let mut g = TrustGraph::new();
g.add_attestation(attest(&venezuela, yumaira.identity_id(), "oficio", "partera"))
.unwrap();
g.add_attestation(attest(&comunidad, yumaira.identity_id(), "oficio", "partera"))
.unwrap();
let (sub, pred, val) = (yumaira.identity_id(), "oficio", "partera");
// Dos terceros: una política laxa acepta, una exigente no.
assert!(g.is_accepted(sub, pred, val, &TrustPolicy::strict(2)));
assert!(!g.is_accepted(sub, pred, val, &TrustPolicy::strict(3)));
}
#[test]
fn empty_corroboration_for_unknown_claim() {
let (yumaira, ..) = actors();
let g = TrustGraph::new();
let c = g.corroboration(yumaira.identity_id(), "nada", "nada");
assert!(c.is_empty() && c.third_party() == 0);
}
#[test]
fn policy_can_accept_self_attestation_when_negotiated() {
let (yumaira, ..) = actors();
let mut g = TrustGraph::new();
g.add_attestation(attest(&yumaira, yumaira.identity_id(), "lema", "sembrar"))
.unwrap();
let lax = TrustPolicy { min_third_party: 0, accept_self: true };
assert!(g.is_accepted(yumaira.identity_id(), "lema", "sembrar", &lax));
// La política por defecto, sin terceros, no la acepta.
assert!(!g.is_accepted(yumaira.identity_id(), "lema", "sembrar", &TrustPolicy::default()));
}
}