CLI de operador: intenciones y grants
Desde v0.12.0 el binario korvun lleva las herramientas de autoridad del
operador: contratos de intención (qué autorizas, con límites) y
grants de autoridad (quién puede actuar bajo una intención, con
menos). Trabajan contra la misma base de datos local que usa el servidor,
con acceso breve y seguro sobre WAL — puedes usarlas mientras Korvun
sirve.
Cada mutación deja un recibo identificado en el registro de acciones: tú
como principal, evidencia loopback en proceso y una regla de auditoría
finita — operator para tus actos, attenuation_violated para una
delegación que el muro rechazó. Los rechazos también se registran: el
rastro dice por qué.
Intenciones
Un contrato de intención declara un resultado autorizado y sus límites: un propósito en palabras, un conjunto de operaciones, un conjunto grueso de recursos, un presupuesto de acciones opcional y una ventana de validez opcional.
korvun intent create --config korvun.json \
--purpose "test week" \
--operations calc,time \
--max-actions 10 \
--expires 2026-09-06T20:00:00Z
Imprime el id nuevo (int_…) en DRAFT. Flags: --purpose y
--operations (separadas por comas) son obligatorias; --resources vale
* por defecto; --max-actions 0 significa ilimitado; --valid-from
por defecto es ahora; sin --expires no hay caducidad.
korvun intent activate --config korvun.json int_…
korvun intent revoke --config korvun.json int_…
korvun intent list --config korvun.json
korvun intent show --config korvun.json int_…
El ciclo de vida falla cerrado y se camina desde el estado ALMACENADO:
DRAFT → ACTIVE → EXPIRED | REVOKED. Lo terminal es terminal — reactivar
una intención revocada falla honestamente, y el intento fallido deja su
recibo. show incluye el digest del contrato: un hash determinista de los
TÉRMINOS (el estado queda fuera — revocar cierra la vida del contrato, no
reescribe su identidad).
Grants
Un grant da a un principal autoridad acotada bajo una intención.
korvun grant issue --config korvun.json \
--intent int_… \
--subject principal_brain_default \
--operations calc \
--max-actions 5 \
--depth 1
La intención debe estar EN VIGOR en el instante de emisión: una intención
DRAFT deniega con intent_inactive, una ventana caducada con
intent_expired — el reloj gana a un estado rancio, y el rechazo queda
registrado.
korvun grant delegate --config korvun.json \
--parent grant_… \
--subject principal_ch_telegram \
--operations calc
Delegar traspasa autoridad — y la autoridad solo puede menguar. El
hijo hereda la intención, la caducidad y el presupuesto del padre salvo
que los estreches con flags, lo emite el subject del padre con profundidad
padre − 1, y debe ser subconjunto de su padre en TODAS las dimensiones:
operaciones, recursos, presupuesto, caducidad, ventana de validez,
profundidad. Un hijo que amplía se deniega nombrando la dimensión ampliada
y jamás toca el disco. El mismo muro gobierna al propio kernel — la CLI
del operador no tiene aquí ningún poder especial.
korvun grant revoke --config korvun.json grant_…
Un grant revocado ya no delega nada (authority_revoked).
Techos de efecto (v0.13.0)
Un grant puede llevar un techo de efecto: la clase de consecuencia
más alta que su autoridad puede alcanzar, sobre la escalera pure < read_external < write_reversible < write_compensatable < write_irreversible < critical.
korvun grant issue --config korvun.json \
--intent int_… \
--subject principal_brain_default \
--operations calc \
--effect-ceiling read_external
Delegar también tiene que menguar aquí: el hijo hereda el techo del
padre salvo que lo estreches, y un hijo que alcance POR ENCIMA se
deniega nombrando effect_ceiling — la décima dimensión de la
atenuación, juzgada por el mismo validador en todas partes. Bajo un
grant con techo (autoridad acotada), las acciones write_irreversible
y critical exigen además aprobación humana. Con approvals.enabled
puesto, esa exigencia APARCA la acción como petición pendiente que
decides en la bandeja de más abajo; sin él —y si el aparcamiento falla—
la llamada sigue muriendo con el no honesto approval_unavailable.
Los grants sin techo (la autoridad permanente de la raíz y los derivados
de config) se comportan exactamente como antes.
El verificador (v0.14.0)
Desde v0.14.0 cada desenlace terminal deja un recibo firmado en una cadena hash de solo-añadir, y la CLI lleva al juez.
korvun receipt verify --config korvun.json rcpt_…
Un recibo (o todos los de un id act_…), re-juzgado offline contra el
fichero del store. Lo que juzga la escalera, en el orden en que lo juzga: el
roundtrip canónico, el hash recomputado, la clave de firma encontrada
en el registro, la firma Ed25519 contra esa clave, la ventana de
validez de la clave, el eslabón de cadena con su predecesor, después
—cuando el recibo sella un digest de aprobación— la aprobación y su
lápida, y en último lugar la coherencia con la fila de la acción. receipt verify imprime TODOS los fallos que encuentra, uno por línea, en ese
orden; korvun ledger check, más abajo, imprime solo el PRIMER fallo
del primer recibo que falla. Cada fallo que juzga la escalera lleva su
nombre (hash_mismatch, signature_invalid, custody_mismatch, …) en
vez de un «invalid» genérico; un recibo cuyos bytes almacenados no
parsean se rechaza con el error de lectura y sin nombre de escalera.
korvun ledger check --config korvun.json
La cadena entera de una partición, estructura primero: un recibo
borrado de DENTRO de la cadena se denuncia por su hueco
(chain_seq_gap con la posición que falta), una posición clonada como
chain_seq_duplicate, y después cada eslabón por los mismos checks —
el PRIMER eslabón roto detiene el veredicto con su id de recibo y su
motivo. Lo que prueba es que el perfil que nombra tu configuración es
consistente consigo mismo: un corte de cola, una cadena re-firmada con
una clave que el atacante registró en ese mismo perfil y una
configuración apuntada a otra tienda sobreviven las tres, y cada una
exige un anclaje externo que Korvun aún no entrega.
korvun receipt rotate-key --config korvun.json
Rotación atómica retira-y-activa de la clave de firma del perfil. El acto de rotación deja SU PROPIO recibo sellado con la clave NUEVA; las claves retiradas se conservan para siempre, así cada era de la cadena verifica con la clave de su era. La verificación es de solo lectura; el alcance honesto está documentado: el libro es tamper-evident, jamás «immutable». El alcance no es un límite de acceso sino una propiedad: los comandos prueban que el perfil es consistente consigo mismo. Las cuatro cosas que NO prueban: que el perfil sea el que escribió tu historia, que la cadena esté completa, que sus claves sean las autoritativas y que una fila cuya columna de búsqueda cambió de clase de almacenamiento esté presente en vez de ausente. Las notas de seguridad del proyecto las llevan enteras.
El buzón de aprobaciones (v0.15.0)
Con approvals.enabled activado, una acción cuya clase de efecto
exige un sí humano ya no muere con el honesto approval_unavailable:
se APARCA como solicitud pendiente con su preview sellado.
Desde la v0.15.0 se decide desde DOS sitios, y es la misma decisión. La pantalla de Aprobaciones de Korvun Desktop lista lo aparcado, enseña el documento entero y toma el sí detrás de una puerta que hay que teclear; esta página documenta la CLI, que hace lo mismo desde un terminal y es la única entrada en un servidor sin pantalla. Las dos tocan el mismo almacén, los mismos cinturones y el mismo claim que consume una vez los parámetros guardados: decida quien decida primero, a la otra se lo dicen por su nombre. Ese consumo único tiene dos límites conocidos. En la v0.15.0, un trigger que restaura los parámetros dentro de la propia transacción del claim lo rompe; está curado en la v0.15.1, la release actual. Y si otra conexión vuelve a escribir los parámetros después de que el claim confirme, mientras la acción siga APPROVED —una primera ejecución aún en curso, una cuyo cierre falló, o un claim seguido de una caída antes del cierre—, una segunda ejecución vuelve a disparar el efecto; una restauración después de que la acción cerrara no obtiene una segunda. Ese segundo límite es un problema conocido fichado para la v0.15.2.
El aparcamiento necesita un cerebro ACOTADO: pon agent.effect_ceiling
en el brain (por ejemplo "write_reversible") — el cable que faltaba
aterrizó con esta etapa: ausente significa sin techo, exactamente como
antes, y entonces nada se aparca.
korvun approvals list --config korvun.json
Cada solicitud con su estado y su caducidad — las consultas van por la puerta de solo lectura: sin migración de esquema, sin recovery, y una conexión sellada que rechaza cada ESCRITURA a nivel de SQLite. No es una promesa de que el fichero en disco quede intacto — la propia apertura crea los sidecars de un store WAL, y reescribe la cabecera de journal de un store dejado en otro modo.
korvun approvals show --config korvun.json apr_…
EL DIGEST que apruebas, primero y bien visible; después el preview
completo — propósito, actor y posición en la delegación, operación,
recursos, qué datos salen, coste, clase de efecto y reversibilidad,
la ley pineada — y los parámetros CRUDOS. Solo se guardan en el almacén
local, y la API de aprobaciones también los sirve, en la dirección del
servidor de administración (observability.addr, loopback por defecto).
korvun approvals approve --config korvun.json apr_…
korvun approvals reject --config korvun.json --comment "why" apr_…
Ambos son actos de operador registrados con su recibo firmado.
Aprobar ejecuta EL objeto guardado — recuperado íntegro, re-verificado
contra el digest aprobado, reclamado atómicamente para que dos
aprobaciones en carrera no obtengan las dos los parámetros (no frente a
una restauración confirmada mientras la acción siga APPROVED, el
problema conocido fichado para la v0.15.2) — y reporta el
desenlace real; el recibo de una acción aprobada sella su referencia
de aprobación (canónico v2), y receipt verify gana el check
approval_mismatch. El rechazo, la cancelación o la caducidad
cierran la acción aparcada con recibo y no queda ningún camino de
ejecución. Las solicitudes caducan por su TTL (por defecto 1h,
approvals.ttl), juzgado al toque de la decisión.
Autoridad estricta (v0.16.0)
Apagada salvo que el perfil la pida. Con ella encendida, un inicio con efecto necesita un principal autenticado y verificado, una intención firmada activa y una cadena de autoridad firmada y activa completa, todo juzgado dentro de la transacción que confirma el inicio. Encenderla son cinco actos del operador, en este orden, y cada uno deja su recibo firmado:
korvun intent create-v2 --config korvun.json --file intent.json
korvun intent activate-v2 --config korvun.json int_pedidos 1
korvun authority admin-issue --config korvun.json --file grant.json \
--reason "why this authority exists"
korvun intent bind --config korvun.json --actor principal_brain_ops \
--channel console --grant grant_pedidos_root int_pedidos 1
korvun authority activate --config korvun.json --profile profile_ops \
--reason "why this profile goes strict"
El último comando imprime un activation_digest. Va a la configuración, y el
perfil es estricto desde el siguiente arranque:
"authority": { "mode": "strict", "activation_digest": "sha256:…" }
authority tiene además issue, delegate, revoke e import-v1, cada uno
con su forma admin- (admin-issue, admin-delegate, admin-revoke). LAS DOS
formas de revoke exigen --reason, y también todas las administrativas; lo
que añaden las administrativas es el operador humano registrado aparte del
emisor del grant. El issue ordinario exige que el actor sea el dueño de la
propia intención, y desde el CLI el actor es siempre principal_local_operator
— así que sobre una intención de otro dueño, el issue ordinario rechaza con
«action/sqlite: authority issuer mismatch» y la puerta es admin-issue.
Lo que la receta de arriba no dice, y hace falta. En un perfil que viene de
una release anterior, el primer comando se rechaza hasta que el servidor haya
arrancado una vez, para levantar el esquema del almacén. grant.json debe
llevar el intent_digest de la versión exacta de la intención, y ningún verbo
del CLI lo imprime: hoy se lee del almacén a mano. Y la forma de intent.json y
grant.json todavía no está documentada, mientras los dos analizadores rechazan
campos desconocidos o repetidos. Las tres cosas quedan fichadas.
Qué debe decir una intención en modo estricto. read_file, http_fetch y
webhook_call son allí de MUNDO CERRADO: solo arrancan bajo términos que
enumeren los recursos, las etiquetas de datos y los destinos que pueden tocar.
Una intención sin allowed_resources no concede ninguno, y el inicio se rechaza
por su nombre: «action: resource out of authority scope», o «action: authority
use unresolved» cuando los argumentos no se pueden resolver — ese texto literal,
no un código corto. Una ruta de read_file que no sea absoluta queda sin
resolver, porque esta capa no conoce la raíz de la jaula a la que la herramienta
la uniría. Y el alcance es de la INTENCIÓN, no de la operación: en cuanto una
intención enumera cualquier recurso, toda operación SIN analizador registrado se
rechaza también — memory_note incluido —, así que una intención con alcance
para read_file cierra en silencio las demás.
Qué ve la persona. Una petición aparcada bajo un perfil estricto lleva el
bloque AUTORIDAD en el documento de aprobación: quién pidió, bajo qué
contrato, por qué cadena de principales, y el presupuesto que quedaba CUANDO SE
APARCÓ — leído de una instantánea firmada y verificado contra esa firma en cada
lectura, no un contador en vivo.
--grant, y qué pasa sin él. El --grant de arriba es lo que ata el grant
firmado al enlace, y es la bandera que decide por qué autoridad resuelve el
perfil. Su valor es el grant_id que hay dentro del fichero que emitiste. El
enlace se rechaza, antes de escribir nada, salvo que se cumpla todo esto:
- el grant está ACTIVO, y también todos los que tiene por encima en su cadena;
- su sujeto es el
--actorque nombras; - todos los grants de esa cadena llevan el
--channelque nombras; - la intención está activa y coincide con el digest contra el que se emitió el grant.
El canal está en esa lista porque el inicio también lo comprueba: un enlace escrito sobre un canal que el grant no lleva se rechazaría en cada arranque, y no hay motivo para dejarte escribirlo.
Ata sin --grant y el enlace no lleva grant, así que un perfil estricto
resuelve su autoridad por la cláusula de configuración derivada de la lista de
herramientas del cerebro mientras el grant que emitiste queda ACTIVO y sin usar.
Nada arranca fuera del alcance de la intención en ninguno de los dos caminos —la
cláusula verifica los mismos términos—, pero la delegación, los grants hijos
atenuados y los presupuestos compartidos con el ancestro solo entran en el
camino de una ejecución por --grant.
Atar otra vez CON --grant reemplaza el enlace DEL MISMO SELECTOR en vez de
fallar: el anterior se conserva como REVOCADO —es el registro de qué autorizó
la acción de ayer— y el nuevo se escribe en la revisión siguiente.
El selector es --actor + --channel + --conversation, y esa tercera parte
importa más de lo que parece. --conversation es opcional; omitida, escribe el
enlace de CUALQUIER-CONVERSACIÓN, y un inicio cae en ese solo cuando la
conversación en la que corre no tiene enlace propio — una conversación nombrada
gana siempre.
Así que las dos direcciones son más estrechas de lo que parecen, y ninguna reemplaza a la otra:
- Atar CON
--conversationreemplaza solo el enlace de esa conversación. Las demás, y el de cualquier-conversación, quedan intactos. - Atar SIN ella reemplaza solo el enlace de cualquier-conversación. Cada conversación con enlace propio sigue resolviendo por el suyo, lo que significa que el grant viejo las sigue autorizando.
No hay un solo comando que reemplace todos los enlaces de un actor y un canal. Si estás rotando un grant porque se comprometió o hay que atenuarlo, vuelve a atar cada conversación que tenga enlace propio, y también el de cualquier-conversación. Listarlos no es posible desde el CLI hoy; los dos huecos están fichados.
La línea revoked binding te dice que el selector que nombraste tenía titular, y
su ausencia te dice que ese selector estaba libre — NO te dice si otros
selectores siguen con el grant viejo.
El comando nombra el reemplazo, y nombra también el enlace anterior cuando lo había:
revoked binding bind_act_5f1ce33dab70ba5918800de9ad4bf067
binding bind_act_3fd7470b7d2e62d0857d8c36daa46b79 -> int_pedidos version 1 ACTIVE under grant grant_pedidos_root
Un primer enlace sobre un selector libre imprime solo la segunda línea.
Atar otra vez SIN --grant NO lo reemplaza. Ese camino es una inserción
llana y el selector ya tiene una fila ACTIVA, así que se detiene en la regla de
unicidad de la base de datos y la imprime en crudo:
korvun intent bind: constraint failed: UNIQUE constraint failed: index 'execution_bindings_active_selector' (2067)
No se escribe ningún enlace y no se pierde nada —el acto rechazado queda en el registro como FALLIDO, que es para lo que ese registro existe—, pero hoy no hay camino de vuelta a la cláusula de configuración desde la línea de comandos. Dar al camino sin grant el mismo reemplazo en sitio queda fichado.
Un --grant con valor vacío es un error de uso, no un enlace sin grant.
Si el grant atado se revoca después, todo inicio bajo ese enlace se rechaza
hasta que vuelvas a atar con otro --grant. El enlace no se repara solo, y
nada más lo repara.
Leer el rastro
Los recibos viven en el registro de acciones junto a todas las demás acciones registradas. Cada fila lleva sus columnas de identidad — principal, intención, autoridad — y su evidencia por intento (proveedor, clase de credencial, subject). Nunca se almacena material secreto: las CLASES de credencial son un enum finito por construcción.