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):
| salida | significado |
|---|---|
0 | totalmente verificado — cada fila revisada, cadena intacta (ok: true) |
1 | roto — se encontró una ruptura (ok: false), o uso incorrecto |
3 | verificado 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:
| herramienta | para | devuelve |
|---|---|---|
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.