Arcaeon Ledger

Un registro de acciones encadenado por hash y a prueba de manipulaciones para agentes de IA, además de un proxy stdio que registra las llamadas a herramientas en el punto de conexión, de modo que el registro no dependa de la cooperación del agente.

Documentación

arcaeon-ledger

Las herramientas de observabilidad te muestran lo que hizo tu agente. arcaeon-ledger te permite probarlo.

Cada registro está encadenado por hash al anterior. Edita una fila, elimínala o reordena el historial, y cada enlace posterior se rompe — verify nombra la línea exacta. Tú eres dueño del registro y puedes probar que no fue alterado. Cero dependencias, un archivo JSONL, dos verbos.

pip install arcaeon-ledger      # then:  from arcaeon_ledger import Ledger
from arcaeon_ledger import Ledger

log = Ledger("agent.log.jsonl")
log.append({"tool": "web.search", "query": "weather in LA", "result_ok": True})
log.append({"tool": "payment", "amount": "49.00", "currency": "USD"})

log.verify()          # VerifyResult(ok=True, rows=2, chained=2, ...)

La manipulación se detecta, no se espera que no ocurra:

# someone edits row 2's amount in the file by hand...
log.verify()          # VerifyResult(ok=False, first_break="line 2: chain mismatch")

CLI (conéctalo a CI o a una compuerta previa al lanzamiento — un registro manipulado sale con código distinto de cero, y un registro que solo pudo ser parcialmente verificado ya no sale como uno totalmente verificado):

python -m arcaeon_ledger.cli append agent.log.jsonl '{"tool":"search","ok":true}'
python -m arcaeon_ledger.cli verify agent.log.jsonl
python -m arcaeon_ledger.cli verify --strict agent.log.jsonl

verify códigos de salida (0.5.7):

salidasignificado
0totalmente verificado — cada fila revisada, cadena intacta (ok: true)
1roto — se encontró una ruptura (ok: false), o uso incorrecto
3verificado solo dentro del alcance (ok: null en cada caso): no se encontró ruptura, pero filas prechain sin encadenar se omitieron sin verificar (verified_scope: "bounded_prechain_skipped"); o las únicas rupturas son declaradas ("bounded_declared_break"); o el archivo tiene cero filas ("empty"). Un prefijo "legado" fabricado cae aquí, nunca en 0. Pasa --strict para convertir los dos primeros en un 1 duro en su lugar.

Una compuerta de CI debe tratar solo 0 como verde:

python -m arcaeon_ledger.cli verify agent.log.jsonl
case $? in
  0) echo "fully verified" ;;
  3) echo "chain intact but prechain rows skipped unverified — inspect, or use --strict" ; exit 1 ;;
  *) echo "ledger broken" ; exit 1 ;;
esac

Prueba quién actuó, no solo el orden

Una cadena de hash prueba la integridad de la secuencia — no puede probar quién escribió cada entrada ni si tenía permiso para hacerlo. Adjunta un bloque authority para vincular al actor y su superficie de permisos dentro de la fila encadenada (a prueba de manipulación):

from arcaeon_ledger import Ledger, authority

log = Ledger("agent.log.jsonl")
log.append(
    {"tool": "payment", "amount": "49.00"},
    authority=authority(
        "agent://billing-7",
        capability_version="v3",              # what they were allowed to do
        tool_schema={"name": "payment", "args": ["amount"]},  # hashed, not just named
        time_source="ntp",                    # trust surface of the clock
    ),
)

Ahora la pregunta de auditoría se agudiza de "¿fue editado?" a "¿fue editado y estaba autorizado el escritor?" — editar el principal, la capacidad o el hash del esquema rompe la cadena como cualquier otra manipulación. Esto compone la evidencia contra manipulación con la reproducción de permisos. (Enviado en respuesta a comentarios de la comunidad en el lanzamiento).

Por qué existe esto

El dolor no atendido más fuerte para los constructores de agentes en 2026 es la brecha de confiabilidad/auditoría: un agente "completa" una tarea y el resultado está silenciosamente mal, y no puedes reconstruir — o probar — lo que realmente sucedió. Las plataformas de observabilidad rastrean ejecuciones; ninguna te da un registro a prueba de manipulación, portátil y propio. La regulación también está llegando: la Ley de IA de la UE exige que los sistemas de alto riesgo permitan técnicamente el registro automático de eventos durante su vida útil (Art. 12(1)) y exige que los proveedores y desplegadores conserven esos registros, en la medida bajo su control, durante al menos seis meses (Art. 19(1), Art. 26(6)). La Ley exige el registro y la retención — la evidencia contra manipulación no es su palabra, es nuestra: cuando alguien pregunta si un registro retenido sigue siendo el registro, esa pregunta necesita una respuesta más fuerte que la confianza. arcaeon-ledger es la versión honesta más pequeña: un registro de acciones criptográficamente encadenado que incorporas, posees y verificas.

Cómo funciona la cadena

chain = sha256(prev_chain + json.dumps(row_without_chain, sort_keys=True, ensure_ascii=False))[:32]

Observa los separadores: el cuerpo de la cadena usa el espaciado predeterminado de Python ", " / ": ", no la forma compacta json-c14n:v1 que usan los resúmenes de artefactos. Un verificador entre lenguajes tiene que reproducir ese espaciado exactamente.

El valor de la cadena es truncated_sha256_128 — los primeros 32 caracteres hex (128 bits) de SHA-256, no el resumen completo. Nombrado así para que nadie lo cite como SHA-256 completo: 128 bits es suficiente para detección de edición/accidente, más delgado si quieres que la cadena en sí sea costosa de triturar después de una reescritura (crédito: revisión de atomic-raven).

Cada fila se compromete con todo el historial anterior. La primera fila se encadena desde una semilla fija "genesis". Las filas sin un campo chain se toleran solo antes de la primera fila encadenada (para que puedas adoptarla en un registro existente); una fila sin encadenar que aparece después de que comienza la cadena se marca ella misma. En un desajuste, la verificación continúa desde el valor reclamado para contar el daño posterior honestamente en lugar de convertir una ruptura en ruido en cascada.

Qué prueba — y las cinco cosas que no prueba

Ser preciso aquí es el producto, no un descargo de responsabilidad. Una cadena de hash prueba que el contenido registrado de cada fila no fue alterado en su lugar después de escribirse: la edición en medio del archivo, la eliminación y el reordenamiento lo rompen, y verify nombra la fila.

Una palabra en esa oración cambió en 0.5.8, y la razón es el tipo de cosa para la que existe esta sección. Solía decir "los bytes registrados", lo que afirma más de lo que la cadena hace. La cadena se calcula sobre cada fila analizada de vuelta del archivo, y el lector normaliza secuencias de bytes que no puede decodificar — así que dos cadenas de bytes diferentes dentro de esa región se leen idénticamente y producen el mismo veredicto. Lo que está protegido es el significado de cada fila, no los bytes exactos del archivo. Si necesitas custodia a nivel de bytes, hashea el archivo mismo junto con esto.

No prueba por sí mismo cinco otras cosas:

1. Truncamiento. Corta las filas más recientes y lo que queda se verifica limpio — ninguna cadena de solo anexión atrapa esto por sí sola. Ciérralo publicando la cabeza en algún lugar fuera de tu propio control, con una cadencia:

pin = log.head().as_pin()
# -> "arcaeon-ledger head chain=9f3c… rows=204 as_of=2026-08-13T17:40:00Z"
# post `pin` to a git commit / public comment / notarization anchor.
# a reader compares a fresh head() against the last pin; a truncated or
# re-minted history disagrees. the MAX gap between pins is your security
# parameter, not the average — an attacker picks the gap.

2. Verdad. La cadena notariza lo que se escribió — un registro a prueba de manipulación de una alucinación sigue siendo una alucinación con un checksum. Para hacer que una fila hable sobre el mundo, hashea un artefacto re-obtenible (URL+bytes, una instantánea, salida de herramienta) y almacena ese resumen en la fila, para que un tercero pueda re-obtenerlo y compararlo.

3. Autoría. authority() (arriba) registra quién-reclamó-qué, pero son datos en la fila, no una firma — un reescritor que re-acuña desde el génesis también lo re-acuña. El anclaje externo de la cabeza (#1) es lo que un re-acuñador no puede avanzar.

4. Prefijo-legado-fabricado. Las filas sin campo chain se toleran antes de la primera fila encadenada — eso es deliberado, para que puedas adoptar la cadena sobre un registro existente sin reescribir su historial. Pero las filas omitidas son filas no verificadas, y el verificador no puede distinguir el historial legado real de un prefijo fabricado. Entonces (0.5.7) una verificación no estricta que omitió filas nunca acuña un verde: ok es None — "no se encontró ruptura, verificado dentro del alcance" — falso, con el alcance en banda (verified_scope: "bounded_prechain_skipped") y el conteo en prechain; el CLI sale con 3, no 0. Solo un escaneo que revisó cada fila devuelve ok=True. Si tu registro está encadenado desde el génesis y no debe tener filas legado legítimas, pasa verify(strict=True) / --strict — trata cualquier fila sin encadenar como una ruptura, rojo duro. (Una fila sin encadenar insertada después de que comienza la cadena ya se marca en cada modo.)

5. Completitud. Esta es la grande, y es estructural: el agente decide sobre qué llamar append. Un registro a prueba de manipulación de las llamadas que un agente eligió reportar sigue siendo auto-reporte. Nada dentro de esta biblioteca puede cerrar eso, porque cualquier cosa que el agente invoque, el agente puede negarse a invocarla.

Ciérralo moviendo la pluma fuera del alcance del agente — registra en la costura en su lugar, en un proceso de SO separado que el agente no posee, no puede omitir y no puede ver:

pip install arcaeon-adapter

python -m arcaeon_adapter --ledger seam.log.jsonl -- <your mcp server command...>

arcaeon-adapter es un proxy stdio que reenvía JSON-RPC byte por byte entre un cliente MCP y un servidor, escribiendo una fila encadenada por hash por cada tools/call en su propio registro. Envolverlo alrededor del propio servidor MCP de esta biblioteca produjo el número que hace el punto: el diario del propio servidor escribió 0 filas mientras que el registro de la costura capturó 5. La brecha entre lo que un sistema reporta sobre sí mismo y lo que la costura observó es lo que vale la pena medir.

Delimitado honestamente, el primitivo es "este archivo no fue reescrito en su lugar" — pequeño, verdadero y comprobable. Las capas superiores (anclaje externo vía head(), vinculación de artefactos, autoría firmada, registro de costura) son cómo lo extiendes hacia una afirmación de evidencia completa.

verify() en registros faltantes o vacíos

Los dos se ven igual ("sin datos"), y verify() los mantiene separados, a propósito:

Ledger("never/written.jsonl").verify()
# VerifyResult(ok=False, rows=0, first_break="unreadable: [Errno 2] No such file...")

open("touched/empty.jsonl", "w").close()
Ledger("touched/empty.jsonl").verify()
# VerifyResult(ok=None, rows=0, chained=0, first_break=None, verified_scope="empty")

Una ruta que nunca se creó no puede ser garantizada — ok=False, "ilegible," igual que cualquier otro fallo de lectura. Una ruta que existe y está genuinamente vacía tiene cero filas para manipular, pero cero filas revisadas tampoco es un verde (desde 0.5.8): ok=None, rows=0, verified_scope="empty", falso, salida CLI 3. La automatización que se ramifica en verify().ok obtiene un rojo para el archivo faltante y un no-aprobado para el vacío; lee first_break y verified_scope para distinguir los dos por nombre.

Cuando el registro se escribió fuera de banda: declara la ruptura, no la re-forjes

Tarde o temprano algo escribe en tu JSONL sin pasar por append() — un script, un incidente, una persona con un editor. La cadena se rompe ahí y permanece rota, porque ese es el registro verdadero. Tus dos opciones obvias son ambas malas: vivir con un rojo permanente que no le dice nada al lector, o recomputar la cadena para que el archivo se ponga verde — lo cual es forjarlo, y una cadena que puedes re-forjar silenciosamente no es evidencia de nada.

declare_break es la tercera opción. Anexa una fila que nombra la ruptura:

from arcaeon_ledger import declare_break, verify_file

declare_break("agent.log.jsonl", 25,
              "Written out of band 2026-08-15 by a session hand-appending JSON "
              "instead of calling append(). Content is true and preserved verbatim; "
              "no chain value was ever computed for it, so none can honestly be supplied.")

r = verify_file("agent.log.jsonl")
r.ok               # None  — bounded, NOT True. Falsy.
r.verified_scope   # "bounded_declared_break"
r.breaks           # 0
r.declared         # ["line 25: declared break (Written out of band 2026-08-15 ...)"]

La ruptura sigue siendo una ruptura, para siempre, en declared. Lo que cambia es que una ruptura conocida y explicada deja de hacerse pasar por una inexplicada — y los bytes exactos del huérfano se fijan con sha256, así que editar esa línea después vuelve a poner el archivo en rojo. Nunca devuelve ok=True. Solo un escaneo que revisó cada fila hace eso, y una fila excusada no fue revisada. verify(strict=True) ignora las declaraciones por completo.

Lo que esto no hace, dicho claramente: es un dispositivo de registro, no uno criptográfico. Cualquiera que pueda escribir el archivo puede escribir una declaración, así que no eleva ninguna barrera contra un atacante que ya tiene acceso de escritura. Defiende contra olvidar, no contra la manipulación. No puede distinguir una anexión fuera de banda honesta de una maliciosa — why es una oración humana no verificada. Y solo puede declarar rupturas que verify() ya encontró; no hace nada sobre rupturas que nadie notó. Úsalo para mantener un incidente honesto legible, nunca como una forma de poner un registro en verde.

Vincula lo que el agente realmente leyó (vinculación de artefactos)

La cadena prueba que una fila no fue editada. No prueba que la fila alguna vez fue verdadera — notarizará una alucinación tan fielmente como un hecho. bind_artefact cierra esa brecha para los casos donde puedes señalar una fuente re-obtenible: hashea los bytes reales que el agente leyó y almacena ese resumen en la fila, para que un tercero pueda re-obtener la fuente y comparar.

from arcaeon_ledger import Ledger, bind_artefact

log = Ledger("agent.log.jsonl")
art = bind_artefact("https://example.com/pricing")   # or bytes, a file path, or a dict
log.append({"tool": "web.read", "url": "https://example.com/pricing", "artefact": art})
# art -> {"subject": {"name": "...", "digest": {"sha256": "..."}},
#         "recipe": "sha256:raw-bytes:v1",
#         "digest": "sha256:raw-bytes:v1:<hex>", "bound_at": "...", "source_meta": {...}}

Los resúmenes son autodescriptivos — nunca un hash hex desnudo. Cada uno es sha256:<recipe>:<version>:<hex>, llevando su propia receta para que un extraño lo reproduzca solo desde la cadena: raw-bytes:v1 (bytes opacos tal como se leyeron) o json-c14n:v1 (una canonicalización JSON fijada y documentada — claves ordenadas, compacta, UTF-8). Las recetas están congeladas y versionadas solo-anexión, así que las filas antiguas conservan su receta para siempre y una regla cambiada nunca hace que el historial parezca manipulado.

Verifica honestamente:

from arcaeon_ledger import verify_artefact

verify_artefact(art)                    # recipe reproducible + string self-consistent
verify_artefact(art, refetch=True)      # for a URL: re-fetch and compare
# -> {"verdict": "live_match",          # <- THE answer; read this field
#     "digest_ok": True, "reason": None,
#     "refetch": "match" | "mismatch" | "unavailable" | "skipped", "notes": [...]}

Lee verdict, no solo digest_ok (0.5.7). digest_ok nombra solo la parte offline — receta reproducible, cadena autoconsistente — y permanece True incluso cuando una re-obtención en vivo no coincide. La etiqueta de nivel superior verdict acuña la respuesta completa en un solo campo: "digest_consistent" (parte offline aprobada, sin comparación en vivo realizada), "live_match", "live_mismatch" (el contenido en vivo ya no coincide — cambiado o manipulado, indeterminado), "live_unavailable" (la verificación en vivo solicitada no pudo ejecutarse), o la razón de fallo tipificada en sí misma cuando la parte offline falla. if out["digest_ok"] después de refetch=True solía leerse verde a través de un desajuste en vivo; out["verdict"] == "live_match" no puede. Una etiqueta que esta compilación no puede reproducir es un fallo tipado, nunca una aprobación. Si el digest nombra un algoritmo, receta o versión de receta fuera del registro admitido, verify_artefact devuelve digest_ok=False con un reason legible por máquina — uno de unknown_algorithm, unknown_recipe, unknown_recipe_version, malformed_digest, subject_digest_mismatch — y nunca llega a la etapa de re-búsqueda, por lo que una receta no verificable no puede volver como "match". Un digest que no podemos recalcular es un digest que no comprobamos, y "no comprobado" no debe informarse como "verificado". Las versiones antiguas siguen siendo verificables al permanecer listadas en SUPPORTED_RECIPE_VERSIONS cuando se acuña una nueva, por lo que la promesa de receta de solo añadido se mantiene sin que el verificador apruebe etiquetas que nunca ha distribuido.

El límite honesto, dicho en voz alta porque es el punto: una re-búsqueda mismatch significa que el contenido cambió o fue manipulado — indeterminado. Nunca se informa como prueba de manipulación. La web muta, da 404s, pone muros de pago y personaliza; el encadenamiento prueba "este es el digest de los bytes que el agente dijo que leyó en el tiempo T," nada más fuerte. Para una captura neutral en lugar de tu propia búsqueda, enruta la fuente a través de una instantánea notarizada; para existía-antes-de-T, ancla el digest externamente. Cada una es una capa que añades — declarada, no implícita.

La comprobación externa: un testigo externo

La cadena no puede detectar la truncación por sí sola — corta las filas más recientes y lo que queda se verifica limpio (declarado en "lo que no prueba", arriba). La solución es un testigo: un registrador fuera de tu propio control que mantiene tu cabeza (rows, chain) en una cadencia. Una vez que un testigo tiene un pin del tiempo T, un registro truncado tiene menos filas de las que el testigo vio, y uno reescrito tiene una cadena diferente en la fila atestiguada. Ninguno puede ocultarse.

from arcaeon_ledger import Ledger, WitnessStore, publish_head, verify_against_witness

log = Ledger("agent.log.jsonl")
witness = WitnessStore("witness_pins.jsonl")   # ideally on a host you don't control

publish_head(witness, "billing-agent", log)    # record the current head — do this on a cadence

# later — did the log survive intact?
v = verify_against_witness(witness, "billing-agent", log)
v.verdict     # "consistent" | "truncated" | "rewritten" | "no_record" | "witness_broken" | "local_broken"
bool(v)       # truthy ONLY on "consistent" — a missing pin is no_record, never a false ok

# READ THE VERDICT WITH ITS QUALIFIERS, never the bare string alone:
v.witness_self_integrity   # "verified" | "unestablished" | "broken"

Un "consistent" desnudo no es la respuesta completa. El veredicto también lleva witness_self_integrity: si el almacén del testigo pudo probar su propia cadena de pines intacta. Un cliente alojado que solo expone latest() no puede auto-verificarse, por lo que sus veredictos se leen unestablished — la comparación se ejecutó honestamente, pero un pin falsificado servido por ese almacén se compararía limpio. verified significa que la cadena propia del almacén fue recalculada; broken significa que falló. Un consumidor que se ramifica en v.verdict == "consistent" sin leer witness_self_integrity está confiando en la honestidad del almacén exactamente tanto como confiaría en la del registro — que es el arreglo que un testigo existe para reemplazar. (Encontrado en la auditoría previa a la invitación del 2026-08-23, C14; el campo existe para que "no comprobado" nunca pueda mostrarse como "comprobado y bien").

WitnessStore es el testigo de referencia: un archivo JSONL de solo añadido de pines. Un testigo alojado es un envoltorio HTTP delgado sobre exactamente este objeto; ejecútalo localmente y tienes un testigo completo, sin conexión y de costo cero que controlas totalmente (con la obvia advertencia de que un testigo que controlas solo es tan independiente como su host).

Lo que esto prueba, exactamente. Un testigo prueba que tu registro no fue truncado o reescrito solo en relación con lo que el testigo vio, y solo tan recientemente como el último pin. Las filas añadidas después del último pin están desprotegidas hasta el siguiente — por lo que la brecha MÁXIMA entre pines es tu parámetro de seguridad real, no el promedio, porque un atacante elige la brecha. Y no dice nada sobre si el contenido registrado era verdadero — ese es el trabajo del encuadernado de artefactos (arriba); el testigo solo protege la forma del historial.

Lo que el testigo mantiene. Solo huellas digitales — (namespace, rows, chain, time) — nunca el contenido de tu registro. Sin contraseñas por diseño: si el testigo es violado, no hay nada sensible que robar, solo hashes inútiles sin el registro original.

Intégralo en cualquier agente MCP

arcaeon-ledger incluye un servidor MCP sin dependencias, por lo que cualquier cliente MCP (Claude Code, etc.) puede dar a su agente un registro a prueba de manipulación sin código. Conéctalo:

{
  "mcpServers": {
    "ledger": {
      "command": "python",
      "args": ["-m", "arcaeon_ledger.mcp_server", "--log", "agent.log.jsonl"]
    }
  }
}

El agente tiene entonces cinco herramientas. Dos son herramientas de operador sobre un archivo: ledger_append(record) para registrar una acción (devuelve su hash de cadena) y ledger_verify(strict?) para probar que el registro está intacto (o obtener la línea exacta manipulada de vuelta). El veredicto de verificación tiene tres valores, igual que la biblioteca: ok: true = cada fila verificada, ok: null = cadena intacta pero filas prechain sin encadenar fueron omitidas sin verificar (verified_scope: "bounded_prechain_skipped" — no es un verde), ok: false = roto. Pasa strict: true para hacer que cualquier fila sin encadenar sea un fallo duro.

Tres son herramientas de agente (0.7.0), para cuando la salida va a alguien — un principal que quiere prueba, o un par que decide si confiar en ti:

herramientaparadevuelve
prove_my_conduct(namespace, events)registrar un lote de lo que acabas de hacer y entregar a tu principal un hash{rows, head_hash, chain_verified}
verify_peer_ledger(jsonl_text, strict?)juzgar el registro exportado de otro agente solo desde su texto{ok, rows, first_break, declared_breaks}
declare_break(namespace, reason)tu registro se rompió — nómbralo en lugar de re-acuñar una cadena{declared_line, declared_breaks, ...}

prove_my_conduct re-verifica después de añadir, por lo que un agente cuyo registro ha sido manipulado obtiene chain_verified: false en lugar de un hash de cabeza con un verde adjunto. verify_peer_ledger devuelve first_break como un número de línea entero (o nulo) para que un agente llamante pueda señalar la fila mala exacta — nunca toca tu registro (la exportación se verifica desde un archivo temporal desechable, y la llamada misma aterriza una fila en <log>.calls.jsonl como cualquier otra llamada de herramienta), y una exportación sin filas analizables devuelve ok: null (verified_scope: "empty"), porque un verde por enviar nada es la falsificación más barata posible. declare_break se niega cuando nada está roto, y nunca restaura un verde.

Los registros de agente viven un archivo por espacio de nombres bajo --ns-dir (por defecto ledgers/ junto a --log). Un espacio de nombres es un nombre, no una ruta: [A-Za-z0-9._-], el recorrido se rechaza en lugar de sanearse.

MCP es JSON-RPC sobre stdio y este servidor lo habla directamente — sin SDK, sin instalación adicional.

Estado

Biblioteca principal, CLI y un servidor MCP listo para usar, todos probados: la biblioteca contra manipulación por edición / eliminación / reordenamiento (test_ledger.py), el servidor MCP a través de tools/list → append → verify en el manejador de solicitudes incluyendo detección de manipulación, y las herramientas de agente contra recorrido de espacios de nombres, manipulación de exportación de pares por línea exacta, y rupturas declaradas (test_agent_tools.py). Extraído de un registro de acciones encadenado por hash en ejecución en producción. El anclaje externo se envía vía head() (publica el pin tú mismo) y el testigo de referencia (WitnessStore, arriba); un nivel de testigo alojado (retención, cadencia de pines automática, exportación de cumplimiento) es la siguiente capa.

MIT.