Demo de Board Council
Resumen
El Board Council es una demo de gobernanza de extremo a extremo en ULedger. Demuestra el patrón de gobernanza de council híbrido: un grupo de principales humanos delega cada uno en un agente de IA votante; los agentes votan de forma autónoma dentro de una ventana de tiempo; un presidente humano finaliza el resultado on-chain.
Cada acción (creación de propuestas, autorización de agentes, envío de votos, finalización) es una transaction firmada en la chain ML-DSA-87. El rastro completo de decisiones está on-chain, con pruebas STARK y es auditable.
Stack:
- Frontend: aplicación Preact TypeScript (Vite, puerto 5177)
- Board driver: backend en Go (puerto 8277): se conecta al clúster de ULedger y gestiona el estado
- Blockchain: clúster de nodes de ULedger (9 nodes, chain ML-DSA-87)
- Contract:
council-voting.wat(smart contract WASM desplegado por el driver al iniciar)
Arquitectura
Browser
│ HTTPS
▼
Vite Dev Server (port 5177)
│ /api/* proxy → ULEDGER_NODE_URL (default http://localhost:8090)
│ /board/* proxy → Board Driver (default http://127.0.0.1:8277)
▼
Board Driver (Go, port 8277)
│
├── GET /state → BoardState (proposals, phase, chain height, nodes)
├── GET /events → SSE stream (vote, new_proposal, finalized, setup_done)
├── POST /proposals → create proposal
└── POST /proposals/{id}/vote → cast human vote
│
▼
ULedger Cluster (9 nodes, docker, ML-DSA-87)
│
├── council-voting.wat (deployed contract)
├── 6 member wallets (human principals)
└── 6 agent wallets (AI directors)
Participantes
| Rol | Identidad | Descripción |
|---|---|---|
| Presidente (Chair) | you (member:alice) | Humano. Crea propuestas. Emite el voto humano final. |
| Director 1 | agent:alice | Agente de IA delegado por alice. Vota de forma autónoma. |
| Director 2 | agent:bob | Agente de IA delegado por bob. Vota de forma autónoma. |
| Director 3 | agent:charlie | Agente de IA delegado por charlie. Vota de forma autónoma. |
| Director 4 | agent:dave | Agente de IA delegado por dave. Vota de forma autónoma. |
| Director 5 | agent:eve | Agente de IA delegado por eve. Vota de forma autónoma. |
| Director 6 | agent:frank | Agente de IA delegado por frank. Vota de forma autónoma. |
Configuración: Qué Ocurre al Iniciar el Driver
Cuando el board driver se inicia, ejecuta automáticamente la secuencia completa de configuración del council:
1. Connect to 9-node Docker cluster (discover ephemeral ports)
2. Register wallets (6 member wallets + 6 agent wallets)
3. Deploy council-voting.wat contract
4. addMember(alice), addMember(bob), ..., addMember(frank) [6 txs]
5. alice: authorizeAgent(agent:alice)
bob: authorizeAgent(agent:bob)
... [6 txs]
6. Phase → "ready"
7. Emit "setup_done" SSE event
Si existe runs/contract-addr.txt, se omite el paso 3 y se reutiliza el contract desplegado previamente.
Ciclo de Vida de la Propuesta
Paso 1: Crear la Propuesta (Presidente Humano)
El presidente escribe un prompt en el frontend y envía una propuesta:
POST /board/proposals
{ "prompt": "Should we expand our product line into the APAC market?" }
El driver:
- Crea la propuesta en el contract:
createProposal(title, description, 10, 2)(ventana de votación de 10 bloques, quórum 2). - Emite work tokens a los 6 agentes:
issueWorkToken(agent:alice, proposalId)× 6. - Emite el evento SSE
new_proposala todos los clientes del navegador.
{
"id": "1",
"title": "APAC Expansion",
"description": "Should we expand our product line into the APAC market?",
"status": "active",
"endBlock": 12345,
"createBlock": 12335
}
Paso 2: Los Directores de IA Votan (Autónomo)
Cada agente (LLM o proceso automatizado) recibe la propuesta y vota:
submitWork(proposalId, vote=1, evidence="Market analysis shows 23% growth potential in APAC...")
El driver orquesta la votación de los agentes de forma concurrente. Cada voto es una transaction firmada independiente desde una billetera de agente independiente. El registro on-chain muestra:
block 12336: agent:alice submitted vote=YES, evidence="..."
block 12337: agent:bob submitted vote=YES, evidence="..."
block 12337: agent:charlie submitted vote=NO, evidence="..."
block 12338: agent:dave submitted vote=YES, evidence="..."
block 12339: agent:eve submitted vote=ABSTAIN, evidence="..."
block 12340: agent:frank submitted vote=YES, evidence="..."
Cada voto se atribuye a la billetera del agente, que es atribuible a la billetera del miembro que lo autorizó, que a su vez es atribuible a la entity PERSONAL (humano) en la jerarquía.
El driver emite un evento SSE vote por cada voto confirmado:
{
"type": "vote",
"proposalId": "1",
"agent": "agent:alice",
"vote": "YES"
}
Paso 3: Vota el Presidente Humano
El frontend muestra al presidente humano un botón YES / NO / ABSTAIN. Este es un voto firmado por un humano:
POST /board/proposals/1/vote
{ "choice": 1 }
El driver envía submitWork(proposalId, 1, "Human vote") desde la alice-wallet (la billetera del miembro, no la billetera del agente; el presidente vota directamente). Este es el punto de intervención human-in-the-loop.
El evento SSE vote del voto humano incluye "agent": "you"; el frontend marca este voto como confirmado cuando recibe el evento.
Paso 4: Avanzar hasta la Fecha Límite
La ventana de votación es de 10 bloques. El driver hace avanzar la chain más allá de endBlock enviando llamadas isMember() sin efecto (no-op) que impulsan la producción de bloques.
Paso 5: Finalizar (Humano o Automatizado)
Una vez superada la fecha límite, cualquier parte puede finalizar:
finalizeProposal(proposalId)
El contract contabiliza los votos y establece el estado:
- Si los votos a favor ≥ quórum:
"passed" - Si los votos en contra ≥ quórum:
"failed" - En caso contrario:
"failed"(quórum insuficiente)
El driver lee el recuento final del estado del contract y emite un evento SSE finalized:
{
"type": "finalized",
"proposalId": "1",
"status": "passed",
"tally": { "yes": 5, "no": 1, "abstain": 1 }
}
El frontend actualiza la tarjeta de la propuesta para mostrar el resultado final, las barras de recuento y la marca de tiempo.
Componentes del Frontend
Barra Lateral: Tarjeta de Miembros
Muestra los 6 miembros con su estado de voto para la propuesta activa:
┌─────────────────────────┐
│ Members │
│ │
│ You (chair-1) [YES] │
│ Bob (director-1) [YES] │
│ Charlie (dir-2) [NO] │
│ Dave (director-3)[YES] │
│ Eve (director-4) [---] │
│ Frank (dir-5) [YES] │
│ │
│ [+ New Proposal] │
└─────────────────────────┘
Principal: Tarjeta de Propuesta Activa
┌─────────────────────────────────────────────────────┐
│ Active Proposal #1 Expires block 12345 6 votes │
│ │
│ APAC Expansion │
│ Should we expand our product line into the APAC... │
│ │
│ Block: ██████████░░░░░ 10/10 │
│ │
│ AI Reasoning: Market analysis shows 23% growth... │
│ │
│ Tally: │
│ YES ████████████░░░ 5 │
│ NO ████░░░░░░░░░░░ 1 │
│ ABS ██░░░░░░░░░░░░░ 1 │
│ │
│ [YES] [NO] [ABSTAIN] │
└─────────────────────────────────────────────────────┘
Overlay de Configuración
Si phase != "ready", se muestra un overlay de pantalla completa:
┌─────────────────────────────────────────────────────┐
│ │
│ Initialising... │
│ Setting up council on ML-DSA-87 chain │
│ │
└─────────────────────────────────────────────────────┘
Ejecución de la Demo
Requisitos Previos
- Clúster de Docker en ejecución:
node-v2-uln-debug-1anode-v2-uln-debug-9 - Go 1.24+
- Node 20+ con pnpm
Iniciar el Board Driver
cd /home/dr/uledger/uledger-apps/board-council/tools/board-driver
go run . 2>&1 | tee /tmp/board-council-driver.log
Flags del driver:
-sdk-root <path>: ruta al golang-sdk (se detecta automáticamente si no se establece)-state-dir <path>: directorio paracontract-addr.txtyvotes.json(por defecto:./runs)-listen <addr>: dirección de enlace (por defecto:127.0.0.1:8277)
Iniciar el Frontend
cd /home/dr/uledger/uledger-apps/board-council
pnpm dev
# → http://localhost:5177
Solución de Problemas
| Síntoma | Solución |
|---|---|
| No se encontraron contenedores | Iniciar primero el clúster de Docker. |
| Driver sin conexión (el frontend muestra un error) | Iniciar el board driver. |
| Atascado en "Initialising" + TRANSACTION_ERROR | Eliminar runs/contract-addr.txt y reiniciar el driver. |
| Error al cargar la billetera | -sdk-root incorrecto o directorio de trabajo incorrecto. |
Estado On-Chain Después de una Ejecución Completa de la Demo
Después de un ciclo de vida completo de una propuesta, el estado on-chain incluye:
contract storage:
"council:member:<alice-addr>" → "1" (6 entries)
"agent:principal:<alice-agent>" → "1" (6 entries)
"proposal:1:status" → "passed"
"proposal:1:title" → "APAC Expansion"
"proposal:1:votes:yes" → "5"
"proposal:1:votes:no" → "1"
"proposal:1:votes:abstain" → "1"
"voted:1:<alice-addr>" → "1" (7 entries: 6 agents + 1 human)
"token:<alice-addr>:1:agent" → "1" (6 work token entries)
Cada escritura on-chain está en los recibos de las transactions, encadenada mediante Merkle y cubierta por el checkpoint STARK de Helix de su epoch.
Para la referencia completa de funciones de council-voting.wat (firmas, claves de estado, reglas de control de acceso) se puede consultar El Contract Council Voting.
Qué Demuestra Esta Demo
- La votación autónoma de agentes es on-chain: Cada voto de agente es una transaction firmada desde una dirección de billetera de agente específica.
- La autorización humana es trazable: El voto de cada agente es trazable hasta la billetera del miembro que llamó a
authorizeAgent, y hasta la entity PERSONAL (humano) que está por encima en la jerarquía. - La intervención humana es explícita: El voto del presidente es una transaction separada, firmada por un humano; es distinguible de los votos de los agentes on-chain.
- La finalización es demostrable: La llamada a
finalizeProposaldel contract produce un resultado determinista y on-chain. El recuento se deriva de los votos, no de un sistema off-chain. - La pista de auditoría está completa: Desde la configuración del council hasta la finalización, cada acción es una transaction firmada. Todo el evento de gobernanza es reconstruible únicamente a partir de datos on-chain.