# Sesiones selladas de extremo a extremo (sealed sessions)

> HTML: https://remote.ivjn.us/docs/es/sealed-sessions/

Android Console abre el cifrado de extremo a extremo desde Ajustes y permite elegir el dispositivo en la página. Los ajustes de cuenta de Android y Web permiten configurar o cambiar la contraseña y vincular o sustituir el correo; el anterior sigue activo hasta verificar el nuevo. Console guarda por defecto la clave raíz en Exchange con frase vacía y conserva las copias existentes protegidas con contraseña. Eliminar la copia desactiva el guardado automático en ese cliente. Una frase vacía permite a Exchange o al poseedor de la base descifrar la clave. La restauración manual verifica la raíz del espacio, la guarda de forma persistente y sincroniza permisos.

Objetivo: aunque el host de Exchange esté comprometido o su operador tenga curiosidad, **no puede leer** comandos, salida, bytes PTY, archivos, capturas ni datos ubus, ni **decidir o falsificar** quién puede ver o administrar dispositivos. Exchange pasa a ser un relé de texto cifrado y metadatos de enrutamiento.

Definiciones: `crates/proto/src/seal.rs` (tipos de protocolo y autorización local), `crates/proto/src/seal/crypto.rs` (criptografía de sesión, solo ring), `crates/proto/src/seal/backup.rs` (copias con contraseña: Argon2id + ChaCha20-Poly1305, no enlazado por agent), `crates/agent/src/seal.rs` (dispositivo), `crates/cli/src/seal.rs` (CLI), `shared/seal-session.js` (sesiones compartidas de Web Console y Android Console, @noble/curves · @noble/hashes · @noble/ciphers), `shared/seal-backup.js` (copias Argon2id sobre las mismas bibliotecas). Vectores entre lenguajes: `shared/seal-vectors.json` (generados por JS; Rust `javascript_vectors_interoperate` comprueba firmas/HKDF/cifrado byte a byte), `shared/seal-rust-vectors.json` (respuesta Rust; JS `shared/seal-session.test.js` negocia y descifra).

`seal approve` sigue publicando permisos automáticamente. Un `seal grant --publish` posterior (o la publicación predeterminada al iniciar sesión) omite un permiso publicado vigente con las mismas claves de emisor y destinatario, dispositivo y level, informa que ya está publicado y no genera otro token. Use `--renew-before DAYS` para renovar permisos próximos a caducar; `--no-publish` sigue firmando tokens sin conexión. Esta comprobación evita duplicados por reintentos secuenciales, pero no ofrece transacciones entre clientes concurrentes ni elimina duplicados históricos. `seal show` crea una identidad local y registra su clave pública si hay sesión iniciada; use un profile separado para pruebas.


## Modelo de amenazas

| | Ruta anterior (en claro) | Sesiones selladas |
|---|---|---|
| Comandos, stdout/stderr, código de salida | Exchange los ve; auditoría completa cifrada con una clave de Exchange | **Invisibles**; auditoría solo de `sealed:true`, clave pública, nivel, caducidad y resultado final |
| Entrada PTY y salida de pantalla | Visibles, auditadas trama a trama | **Invisibles**, tramas cifradas excluidas de auditoría |
| Archivos, capturas, árbol de interfaz, parámetros/resultados ubus | Visibles | **Invisibles** |
| Quién puede hacer qué (superadministrador/administrador/operador/lectura, ver/administrar) | Decide Exchange | **Decide el dispositivo**: solo acepta autorizaciones firmadas por la raíz fijada; Exchange no puede emitirlas, ampliarlas ni falsificarlas |
| Suplantar dispositivo | Posible (Exchange puede responder) | Imposible: identidad nativa firmada y clave fijada en CLI; extensión verifica cadena raíz y firma de respuesta |
| Inyectar pulsaciones/comandos | Posible | Imposible: AEAD y contadores estrictamente crecientes; agent descarta texto en claro dirigido a IDs sellados |
| Repetición, reordenación, pérdida, truncamiento | — | Detectados; cierran sesión (fail closed) |
| Metadatos visibles | — | ID dispositivo, cuenta, ID sesión, tiempos, tamaños y cantidades; stdout/stderr indistinguibles porque el tipo va cifrado |
| Acciones posibles | — | Denegación de servicio: retener, desconectar, cerrar (`sealed_close`); afecta disponibilidad, no permisos |

Fuera de alcance: dispositivo o equipo del operador comprometido; suplantación de Exchange en la primera conexión TOFU, evitable comparando huellas por otro canal. Todas las operaciones de dispositivo solo aceptan sesiones selladas; los límites del catálogo de comandos y de la autorización raíz de navegador figuran debajo.

## Cobertura de rutas y diseño

| Ruta | Comportamiento de sesiones selladas | Autorización y límites del dispositivo |
|---|---|---|
| HTTP `POST /api/devices/{id}/exec` | **Rechazo explícito**, HTTP 409, indica utilizar una sesión WebSocket `open_sealed` | Aunque Exchange ignore la política, el dispositivo rechaza `exec_req` en claro |
| HTTP comandos predefinidos `/commands/{cmd}/run` | **Rechazo explícito**, con la misma indicación de WS sellado | El catálogo de servidor no autoriza al dispositivo |
| Comandos predefinidos Web Console / CLI | Obtiene plantilla; el operador envía `exec_req` y recibe salida en WS sellado | Verifica hello, grant raíz, revocación, vigencia y `manage`; `view` no ejecuta plantillas |
| LAN `/ws/operator` | Solo sesiones selladas; hello incluye grant y cadena; sin token Bearer | Misma cadena raíz y view/manage; exterior solo dirección privada/Origin. `/api/devices` devuelve ID y clave/modo seal |
| Agent navegador / rutas nativas selladas | `open_sealed` / `sealed_input` / `sealed_event`; rechaza canal browser antiguo | Extensión verifica grant/cadena raíz; CLI/Console verifican cadena de `SealAccept.peer` y coincidencia con clave firmante |
| CLI `exec-attach` / `exec-hold-tail` / `exec-hold-cat` | Peticiones, salida y final sellados nativos | Requiere `manage`; los trabajos sellados no pueden leerse ni adjuntarse en claro |
| CLI `sensors --live` | Una sesión sellada por muestra (una operación/sesión); Ctrl-C termina la actualización | Basta `view`; no agrupa muestras en un túnel de una sola operación |
| CLI `net` | Sella `net_req` / `net_resp`, URL, cabeceras, cuerpo y resultados | Requiere `manage` |
| CLI `device update` | Sella `update_req`, todo el progreso y el estado final | Requiere `manage`; `update` superior actualiza el cliente local, no una sesión remota |
| exec / PTY / file / ubus / Android / sensors individual / hold list, kill | Implementación sellada nativa existente, sin cambios | Mismo `required_level` y dispatcher del dispositivo |
| Dispositivos, métricas, conexión, sessions Exchange | Metadatos de rutas/monitorización, no operaciones selladas de dispositivo | Visibles a Exchange; no autorizan operaciones |

### HTTP y comandos predefinidos

Las operaciones selladas nativas ya admiten clientes que usan únicamente HTTP. Use `remote-cli --transport http` o `REMOTE_TRANSPORT=http`. `POST /api/sealed/connections` crea una conexión; `POST /input` envía la negociación firmada o el texto cifrado, `GET /events` obtiene resultados y `DELETE` cierra la conexión. El cliente mantiene la criptografía y la verificación de autorizaciones. Los antiguos endpoints de acciones siguen devolviendo 409; la conexión del dispositivo no cambia.

https://remote.ivjn.us/sealed-http.md


Crear, editar y listar predefinidos sigue siendo administración de Exchange. **Plantillas, nombres y descripciones no son datos sellados ni una lista permitida firmada por raíz**. Console/CLI con claves ejecutan plantillas como exec sellado normal sin hold. Dispositivo solo confía en política local y grant, no en catálogo, rol o etiqueta «predefinido». `view` no ejecuta: necesita `manage` de raíz. Alterar catálogo no amplía grant, pero `manage` permite exec arbitrario: revise plantillas y no almacene secretos allí. Salida se descifra localmente; auditoría solo contiene metadatos sellados.

### LAN y selección del cliente

Las sesiones de dispositivo de la CLI utilizan exclusivamente sellado de extremo a extremo, sin depender de que Exchange anuncie `seal_key`, capacidades o indicaciones de política. La información de capacidades ausente o eliminada no degrada a texto en claro. Si falla la lectura de la base de datos local de claves fijadas, se devuelve un error sin ignorar registros de confianza dañados.

Exec sellado con/sin hold y tareas de archivos se marcan antes de publicar globalmente: cerrar antes del primer evento saliente no expone vistas previas mediante carreras. Registros omiten destino/método HTTP y objeto/método ubus.

LAN reutiliza envelope, transcript, contadores, AEAD, fijación y revocación. Salida pasa por `Tunnels::seal_outbound` antes de `OperatorDown`; dispatcher descarta inyección en claro a IDs sellados. Cerrar separa held exec, cancela otros; IDs aislados descartan salida tardía. Sin token LAN: hello lleva grant/cadena en caché y el mismo `authorize` decide view/manage. Exterior comprueba dirección privada y Origin. Siempre rechaza texto en claro, independientemente del valor histórico de la política. Web alojada sigue usando Exchange.

La información opcional del dispositivo `info.seal_mode` (`allow` / `require`), `seal_key` y la capacidad `sealed` conservan la compatibilidad. No constituyen autorización ni una clave pública de dispositivo de confianza; la política la aplica finalmente el dispositivo. Los comandos, terminales, archivos, ubus, sensores, trabajos, sondeos de red, escritorio, actualizaciones de dispositivos, comandos predefinidos y operaciones de navegador de CLI, Web Console y Android Console utilizan exclusivamente sesiones selladas nativas, sin ruta en claro. El valor `allow` también rechaza operaciones sin cifrar. Los fallos de negociación, fijación del dispositivo, grant o descifrado detienen la operación sin reintentar en claro. Tras cambiar la política en ejecución, la información LAN se actualiza inmediatamente y la información en la nube al reconectar y registrar.

### Autenticación del Agent de navegador con cadena raíz del espacio de trabajo

La extensión anuncia `sealed`, su `seal_key` Ed25519 y `seal_mode: require`. Tras emparejar o configurar el dispositivo, llama a `POST /api/seal/device` con credenciales de dispositivo y una prueba de posesión firmada. Exchange determina el espacio desde el dispositivo y vincula su clave pública. Un cliente con la clave privada raíz de confianza firma un grant view para ese dispositivo exacto. El registro y el bearer de dispositivo permiten acceder al relay y obtener credenciales firmadas; las sesiones requieren autenticación por certificados.

La extensión verifica firma, cadena raíz, dispositivo, nivel, vigencia y revocaciones locales; después firma `SealAccept` con su propio `SealHello` firmado en `peer`. CLI `finish_workspace_operator` y `finishBrowser` verifican la prueba, exigen `peer.operator_key == accept.device_key` y comprueban la firma de respuesta. Una prueba ausente o inválida cierra la sesión. Las sesiones válidas comienzan automáticamente. Raw CDP también exige consentimiento local por solicitud o permiso desatendido limitado a clave del operador, servidor y dispositivo; view no permite CDP.

Los permisos se separan por origen del servidor, espacio y dispositivo. La semilla de identidad local solo es accesible desde contextos de extensión de confianza. La primera raíz se fija localmente y sus cambios se rechazan. CLI y Console usan su identidad seal y cadena raíz normales, sin editor de pin exclusivo del navegador. Firma raíz, permisos de certificado y credenciales de acceso del dispositivo son comprobaciones distintas; el TOFU de dispositivos nativos sigue separado.

**Límite de seguridad:** primer TOFU no detecta raíz falsa de relé malicioso: compare por otro canal. PoP prueba posesión, no aprobación. Aprobación automática depende del listado: instalaciones sensibles deben revisar claves. Proteja profile/semilla y publicación de Console; endpoints comprometidos o full CDP para operadores maliciosos quedan fuera. Borrar catálogo no revoca credenciales cacheadas; importan revocación local/caducidad. Exchange ve metadatos, puede retener credenciales y denegar servicio.

**Identidad no disponible:** si la identidad del dispositivo no puede cargarse o escribirse, las sesiones selladas no están disponibles y se siguen rechazando todas las operaciones en claro (fallo cerrado). La ausencia de archivo de política, una política dañada o sus valores antiguos no habilitan una ruta sin cifrar; debe repararse el almacenamiento de claves en el propio dispositivo.

Las peticiones HTTP deben llegar a la instancia Exchange que creó la conexión. Esta caduca tras 90 segundos sin actividad. Envíe las entradas en serie y reintente con el mismo número y texto cifrado. Confirme los eventos procesados con `after`. Web Console, móvil y LAN conservan su transporte. Las órdenes predefinidas siguen requiriendo `manage`.

### Verificación de regresiones

Rust cubre negociación real de grants, resultados sellados exec/attach/tail/cat/sensors/net/update, rechazo del texto en claro con cualquier valor de política, negociación/tramas/política LAN WS, HTTP 409, cierre browser require y flags/información compatibles. JavaScript cubre vectores Rust/JS, envelopes, salida sellada predefinida y ausencia de degradación, configuración/registro/rechazo/política navegador y errores de fijación. Singleton seal se inicializa en subprocesos aislados para evitar interferencia concurrente.

```sh
CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0 cargo test --workspace
cargo fmt --all --check
CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0 cargo clippy --workspace --all-targets -- -D warnings
npm test --prefix shared
npm test --prefix website/console
npm test --prefix browser-agent
node --test scripts/test-pages-worker.mjs
```

JavaScript usa Node 24+. LAN nueva realiza HTTP Upgrade / WebSocket real sobre duplex en memoria: verifica negociación/tramas/require/allow sin sockets de red. Pruebas loopback WS/TLS/HTTP existentes requieren enlazar puertos; sin permiso fallan `Operation not permitted` y no prueban éxito completo. Red/actualización comprueban éxito/error sellados sin descargar/instalar actualizaciones.

**Riesgo Web Console:** Pages entrega código; sitio/CDN comprometido puede leer texto en claro o firmar con claves del navegador (sin extraer privadas no exportables). Confidencialidad frente a Exchange requiere confiar en publicación estática; si no, use CLI o Android Console.

## Identidades y claves

| Clave | Tipo | Almacenamiento | Visibilidad pública |
|---|---|---|---|
| Dispositivo | Ed25519 | agent `agent.seal.key` (0600); estado tmpfs (`/var/run`, `/run`, `/tmp`) usa `/etc/remote-agent/`, configurable `REMOTE_SEAL_DIR`; incluido en conffiles OpenWrt | Registro `info.seal_key`, solo visualización; CLI no confía en copia |
| Operador | Ed25519 | CLI `seal-identity.pk8` junto a `config.toml` (0600); Web IndexedDB `remote-seal`, WebCrypto Ed25519 `CryptoKeyPair` **no exportable** (semilla aleatoria IndexedDB si no compatible); Android semilla `expo-secure-store` cifrada Android Keystore | Negociación |
| Raíz | Ed25519 | Clave del superadministrador; Web requiere semilla exportable y sustituye WebCrypto no exportable al generar raíz; exportable `rsk1.`, copia Exchange automática de Console con contraseña vacía | Registro `rsr1.` publicado/fijado |
| Sesión | X25519 efímero → HKDF-SHA256 → ChaCha20-Poly1305 por dirección | Solo memoria, descartada al terminar (secreto hacia adelante) | Nadie |

Las claves privadas del operador se guardan localmente. Console sube por defecto una copia de la raíz con contraseña vacía; Exchange o quien posea la base puede descifrarla. Puede establecerse una contraseña manualmente. Borrar la copia desactiva el guardado automático en este cliente.

## Autorización (Exchange no es autoridad)

Dispositivo confía en **una raíz pública del espacio**. Política `agent.seal.json`:

```json
{"mode":"require","root_key":"<base64 Ed25519>","revoked_keys":[]}
```

Agent decide nivel por `sealed_open`:

1. Sin raíz fijada → rechazo (`no workspace root pinned on this device`).
2. Operador en `revoked_keys` → rechazo.
3. Operador igual a `root_key` → `owner` (administrar y cambiar política).
4. Con `grant`: firma válida, `operator_key` coincide, `device_id` local o `*`, vigente (reloj ±5 minutos). Firma raíz fijada o cadena `rsc1.` en `hello.chain` (fuera de firma hello), orden raíz→firmante; cada certificado permite emitir y no supera profundidad/nivel/ámbito/caducidad del anterior. Solo `view` o `manage`, nunca `owner`.
5. Resto → rechazo.

## Restricciones de cadena de certificados

Se exige cadena `rsc1.` hasta raíz fijada (`hello.chain`, raíz→firmante), no intermedio aislado. **Sin máximo funcional** de niveles (pueden superar 10); barrera DoS: `MAX_CERT_CHAIN` **64**, tamaño canónico total `rsc1.` `MAX_CERT_CHAIN_BYTES` ≤ **64 KiB** (`check_chain_limits` / `checkChainLimits` en `crates/proto/src/seal.rs` / `shared/seal-cert.js`, iguales en dispositivo/Exchange/CLI/Web/Android). Excesos se rechazan.

`max_path_len` (0–255) es restricción **opcional** del emisor sobre certificados inferiores. `seal cert issue` sin `--max-path` usa toda profundidad restante (255 prácticamente ilimitado). La cantidad restante no puede superar `max_path_len`. El `max_path_len` del intermedio de administrador es 0: firma grants, no certificados. `can_issue` falso también impide emitir.

Hijos deben quedar dentro del padre (`cert_within_parent` / `verify_chain`):

- **Nivel:** raíz `owner`; certificados/grants solo `view` o `manage`, nunca superiores al padre ni `owner`.
- **Ámbito:** subconjunto del padre; solo padre `*` permite todos; en otro caso cada dispositivo hijo debe figurar en padre. Hijo `*` exige padre `*`.
- **Caducidad:** no posterior al padre; raíz sin máximo. Grant tampoco supera vigencia ni ámbito del emisor.

**Revocar un intermedio invalida toda su rama.** Se compara `subject_key` y `serial` de cada certificado con `revoked_keys`; cadenas con intermedio revocado fallan, incluidos certificados descendientes y grants. Borrar en Exchange solo detiene distribución: copias siguen válidas hasta caducar. Para invalidar de inmediato añada clave sujeto o serie a `revoked_keys` local.

`proto::seal::required_level`: `view` permite sensores, lista hold, Android lectura (status/tree/screenshot/find_text); exec, PTY, archivos, ubus, red, autoactualización y entrada Android requieren `manage`.

Grant es JSON firmado por raíz: alterar nivel, dispositivo, caducidad o destinatario invalida firma; hello lo vincula a sesión. Rol Exchange solo decide enrutamiento (visibilidad tenant), sin cambiar permisos sellados. Sin reclamar/propietario/copropietario por dispositivo: único `owner` es raíz.

**Cómo fija raíz:**

- **TOFU:** Exchange envía `seal_root {token, rotation?}` con registro autofirmado `rsr1.`; dispositivo verifica y fija en `agent.seal.json` si falta raíz.
- **Rotación:** acepta cambio solo con `rsx1.` **firmado por raíz fijada**, `old_root` actual, `new_root` nueva, mismo espacio; si no ignora y registra. También `seal_policy_set {root_key}` sellado (solo raíz actual `owner`).
- **Local:** `remote-agent seal root <key>` escribe; `remote-agent seal forget-root` elimina para nuevo TOFU al reconectar (reiniciar agent).

**Riesgo:** Exchange comprometido en primer TOFU puede imponer raíz falsa. Fije antes con `remote-agent seal root <key>` (huella `remote-cli seal root`); después Exchange no puede sustituirla.

**Revocación:** raíz `remote-cli seal policy <设备> --revoke <key>` o local `remote-agent seal revoke <key>`. Raíz no revocable. Grant dura 90 días por defecto. Borrarlo de Exchange solo detiene distribución; copias importadas requieren revocación local antes de caducar.

## Exchange como relé de autorizaciones (no fiable)

No hace falta copiar `rsg1.` ni claves en chats: **directorio de claves** y **relé de grants** por espacio. No cambia amenazas: Exchange no lee ni emite/amplía/falsifica. Guarda:

1. Clave Ed25519 con posesión demostrada localmente (`POST /api/seal/keys`).
2. `rsg1.` firmado original (`POST /api/seal/grants`).
3. Intermedio `rsc1.` original (`POST /api/seal/certs`); verifica firma Ed25519 sin poder falsificar. `GET /api/seal/certs/chain?subject_key=` obtiene cadena raíz primero, limitada a 64/64 KiB.

Dispositivo sigue exigiendo raíz fijada o cadena `rsc1.` hasta ella. Exchange analiza para indexar `owner_key` / `operator_key` / caducidad; **subir no autoriza**. Puede retener (`no grant`), no cambiar `view` a `manage` ni suplantar raíz.

Cualquier miembro sube grants: firma determina validez; carga no autoriza. CLI autenticado usa `--publish`; Web/Android «Emitir y publicar» usa mismo endpoint.

Prueba de posesión vincula:

```text
remote-seal-key-register:v1:<workspace_id>:<operator_id>:<public_key>:<issued_at_ms>
```

Firma Ed25519 con esa clave; `issued_at_ms` dentro de ±10 minutos. CLI intenta registro en `login`/`seal show`; Console tras login.

Sin grant local coincidente intenta `seal sync`; rechazo `no grant` permite otra sincronización y un reintento.

Emisor online renueva grants publicados próximos a caducar, firma local; Exchange guarda nuevos `rsg1.` y elimina antiguos, sin cambiar nivel/destinatario.

- **Cuándo:** CLI tras login y al iniciar comandos autenticados (`seal renew` manual sin doble renovación); Web al entrar al espacio incluso sesión ya existente; Android tras login y primer plano. Sin sesión/grants propios no renueva.
- **Reglas:** `GET /api/seal/grants?owner_key=<本机公钥>`; vencimiento en 30 días reemite 90 con mismos `device_id`, `operator_key`, `level`; publica antes de `DELETE` antiguo. Si `GET /api/seal/keys` funciona omite `operator_key` ausentes; si falla no filtra.
- **Frecuencia:** Web/Android: una vez por emisor/12 horas; CLI `seal-renew-state.json`, Web `localStorage`, Android SecureStore. Fallo no bloquea: stderr/`console.warn`; éxito CLI avisa brevemente. Web raíz al entrar y Android al abrir «Cifrado de extremo a extremo» emiten inicialmente sin límite; siguientes refrescos de visita sí limitados.
- **Renovación CLI:** el éxito se registra por servidor y clave emisora. Tras el éxito se omiten las solicitudes de renovación durante 12 horas; un fallo o interrupción solo espera 5 minutos antes de reintentar. El proceso completo tiene un límite de 30 segundos. Los errores indican etapa y duración; si falla el comando principal, la renovación se cancela inmediatamente. Las revocaciones pendientes omiten ese límite de frecuencia y conservan un presupuesto independiente de 4 segundos. Renovación explícita: `remote-cli seal renew`. Un inicio de sesión correcto fuerza una actualización.
- **Raíz:** superadministrador usa emisión por roles siguiente para no recrear grants eliminados.
- **Manual:** `remote-cli seal renew`, defecto `--before 30 --days 90`, mismas reglas.

```bash
# 按工作区用户名授权对方每一把已登记的钥匙，并发布到 Exchange（默认 90 天）
remote-cli seal grant --device lab-router --to alice --level view
# 只打印 token，不上传
remote-cli seal grant --device lab-router --operator-key <KEY> --no-publish
remote-cli seal keys          # 工作区公钥目录
remote-cli seal sync          # 拉取签给自己钥匙的 grant 并导入本地
remote-cli seal renew         # 手动续签，默认 --before 30 --days 90
```

## Superadministrador del espacio como raíz de confianza

Raíz es Ed25519 del cliente superadministrador. Dispositivos aceptan esa raíz y `rsg1.` verificable mediante `rsc1.`. Cliente emite/renueva según rol. Exchange solo guarda `rsr1.` autofirmado, `rsx1.` y `rsi1.`, verifica formato/firmas, **no decide aprobación**. Puede retener o insertar miembros falsos, no falsificar raíz. CLI/Web/Android aprueban automáticamente por defecto; nuevas claves del listado reciben firma. Solo desactivar deja pendientes claves nunca aprobadas/publicadas por raíz.

### Modelo de confianza y compromisos

- **TOFU raíz.** Primera raíz válida se fija. CLI `seal-root.json` junto a `config.toml` (0600), clave `服务器|工作区 id`; Web `localStorage` (`remote.seal.root.<工作区 id>`); Android SecureStore `remote.seal.rootstate`, JSON por servidor/espacio. Después verifica firma/huella. Cambios advierten: **cliente no confía automáticamente** (dispositivo sí con `rsx1.`). Compare y use `remote-cli seal root --trust <key>` o «Confiar en nueva raíz».
- **Invitación solo abre registro.** Raíz firma `rsi1.` (espacio, raíz, nonce 16 bytes, caducidad). Administrador/operador/lectura obtienen al login, comandos, carga o primer plano, verifican con raíz **fijada** y registran vía `POST /api/seal/keys`. No autoriza.
- **Emisión por rol, aprobación local.** Raíz lee `GET /api/seal/roster` (solo superadministrador; desactivados, roles, `operator_device_grants`, claves) y grants propios, calcula:
  - Superadministrador/administrador sin restricciones: `manage` `*`.
  - Administrador restringido: dispositivo delegado, `manage` si `can_manage`, si no `view`.
  - Operador no restringido: `view` `*` más `manage` en dispositivos `can_manage`.
  - Operador restringido: `manage`/`view` por delegación.
  - Lectura no restringida: `view` `*`.
  - Lectura restringida: `view` por dispositivo; ni `can_manage` concede `manage`.
  - Desactivados: nada.
  - Defecto 90 días; renueva con menos de 30.
- **Intermedios de administrador.** Raíz emite `rsc1.` a activos (`manage`, `can_issue`, `max_path_len` 0, 90 días); ámbito `*` sin restricciones, si no dispositivos delegados `can_manage`. Renueva a 30 días; pendientes no reciben. Administrador con intermedio vigente firma grants a miembros gestionados en intersección certificado/delegación; omite si grant raíz reciente cubre operador/dispositivo/nivel (`*` cubre específico, `manage` cubre `view`). Retirar de Exchange solo frena distribución; copias válidas hasta caducar. Revocación efectiva mediante `revoked_keys` local.
- **Aprobación automática predeterminada.** Web `localStorage` y Android SecureStore interpretan ausente como activado; `false` explícito permanece desactivado. Es **más débil**: Exchange comprometido inserta miembros y obtiene firma inmediata. Interruptor «Aprobar automáticamente nuevas claves públicas». CLI está activado por defecto, igual que Web/Android Console: si falta el archivo `seal-approved.json` o el campo `auto_approve`, se considera activado; `"auto_approve": false` explícito permanece desactivado. Use `remote-cli seal approve --no-auto-approve` para desactivar o `remote-cli seal approve --auto-approve` para reactivar; la elección se guarda. Desactivado, claves nunca aprobadas/publicadas van a pendientes locales (CLI `seal-approved.json`, Web `localStorage`, Android SecureStore). `remote-cli seal pending` muestra usuario/huella; `seal approve --all` o `seal approve <key>…` aprueban/emiten inmediatamente. Ya aprobadas/publicadas renuevan/adaptan roles automáticamente. Clave propia no requiere aprobación.
- **Emitir al abrir.** Web raíz al entrar y Android al abrir panel emiten sin límite 12 horas inicial; refrescos posteriores sí limitados. Cambiar interruptor o «Aprobar» fuerza emisión.
- **Cuenta, insignia y errores.** Web en diálogo «Cuenta» de barra del dispositivo coloca interruptor/pendientes; insignia «N claves públicas pendientes de aprobación» abre diálogo. Error en barra «Error de emisión de raíz del espacio: …», eliminando tokens y claves largas. Android muestra equivalentes en panel de cifrado.
- **Reducir rol no revoca grants anteriores.** Miembro eliminado/desactivado o clave ausente: borra grants, deja renovar e intenta `revoked_keys` en dispositivos online compatibles. Reducir rol solo cambia grant, no añade la clave a `revoked_keys`; anteriores importados siguen vigentes. Nunca revoca raíz. Trabajo no terminado en 4 segundos queda local para siguiente comando, sin límite 12 horas. Clave nuevamente autorizada se desrevoca solo donde se revocó correctamente.
- **Registro raíz automático.** Al crear superadministrador (`register`/primera cuenta) o login posterior, sin raíz en Exchange cliente CLI/Web/Android publica su clave y una invitación 90 días. Invitación ausente o caduca en 30 días se refresca según límite de emisión. Manual `remote-cli seal root --publish` disponible.

### Configuración inicial

1. Superadministrador registra/inicia sesión: genera/publica raíz (CLI stderr, Console aviso).
2. Custodie raíz (una o ambas):
   - Sin subir: `remote-cli seal root --export root.rsk` (0600, `-` stdout) o «Exportar raíz privada».
   - Configuración manual de la copia nube: `remote-cli seal backup` o panel de copia; contraseña vacía permitida, pero **vacía/débil permite a Exchange descifrar raíz y controlar todos los dispositivos**.
3. Compatibles fijan raíz al conectar (TOFU); más seguro ejecutar antes `remote-agent seal root <根公钥>`.
4. Miembros registran claves al login. CLI/Web/Android autoaprueban por defecto. Si está desactivado, `remote-cli seal pending`, luego `remote-cli seal approve --all` o «Aprobar» emite inmediatamente.

```bash
remote-cli seal root                       # 显示已固定的根公钥和指纹
remote-cli seal root --export root.rsk     # 导出根私钥（rsk1. 文本）
remote-cli seal root --import root.rsk     # 在另一台机器装上根私钥（或 seal restore）
remote-cli seal root --rotate              # 生成新根，旧根签 rsx1. 轮换，自动按新根重签成员授权
remote-cli seal invite                     # 单独刷新邀请（默认 90 天，--days N）
remote-cli seal pending
remote-cli seal approve --all              # 或 seal approve <key>…；--no-auto-approve 关闭自动批准（保持开启较弱）
```

### Cambiar máquina / raíz

- **Otra máquina:** superadministrador sin raíz publicada local y copia `rsb1.` en Exchange **intenta restaurar automáticamente** (véase sección siguiente). Si no, solicita `seal root --import <文件|->`, pegar `rsk1.` en Console o restaurar copia (`seal restore` / «Restaurar desde Exchange»). Importar raíz actual solo instala localmente.
- **Rotación:** `seal root --rotate` o importar `rsk1.` **diferente**. Con raíz actual, firma `rsx1.` y publica junto a `rsr1.` mediante `PUT /api/seal/root`; online verifica firma antigua y cambia, offline recibe al reconectar. Aprobaciones previas permanecen locales y grants se firman con nueva raíz (90 días). Antigua guardada `seal-identity.<毫秒>.previous.pk8`. Reexporte/respalde nueva.
- **Antigua perdida:** no firma rotación. Publica nueva, pero dispositivos fijados no aceptan; ejecute `remote-agent seal root <新根公钥>` y reinicie en cada uno.
- **Transferir superadministrador:** véase transferencia raíz; cambiar rol no entrega clave fiable.

## Restauración automática de raíz privada al iniciar sesión

Tras login superadministrador, **antes de crear identidad local**, CLI/Web/Android usan misma decisión pura (`decide_auto_restore` Rust, `decideAutoRestore` JS) para restaurar `rsb1.`:

1. **Omitir:** no superadministrador, ya posee raíz publicada, nube desactivada (CLI `REMOTE_SEAL_CLOUD_BACKUP=0`; Web `localStorage.seal.cloudBackup=off`; Android `remote.seal.cloudBackup=off`), o raíz no publicada.
2. **Obtener una vez:** `GET /api/seal/backup`, limitado por hora. Sin copia informa raíz en otro lugar sin bloquear.
3. **Contraseña vacía silenciosa:** si descifra, coincide raíz publicada y no hay otra clave local, instala/fija.
4. **Si no, contraseña:** CLI prueba `REMOTE_SEAL_BACKUP_PASSPHRASE`, entrada oculta TTY; Web/Android aviso **no bloqueante** para escribir en panel y «Restaurar desde Exchange». Sin diálogo bloqueante; login termina.
5. **Seguridad:** nunca sobrescribe otra clave silenciosamente (requiere `seal restore --force` o confirmación); raíz debe coincidir; fallo solo advierte; nunca imprime contraseña, semilla o cifrado `rsb1.`.
6. **Riesgo no bloqueante:** avisa que varias máquinas con raíz aumentan exposición; éxito sin contraseña advierte que Exchange puede descifrarla.

## Transferir superadministrador = transferir raíz

Rol y raíz son distintos. Solo transferir rol no entrega clave ni cambia confianza. Entregue **raíz privada** y rote inmediatamente para invalidar copia entregada.

1. Titular con raíz usa `POST /api/superadmin/transfer`, `{operator_id, root_handover?}`. `root_handover` es `rsb1.` cifrado como backup (Argon2id 64 MiB, t=3, p=1, ChaCha20-Poly1305), contraseña puede ser vacía. CLI `remote-cli seal handover <账号>`; Web/Android solicitan contraseña opcional y adjuntan si poseen raíz.
2. Exchange **guarda sin descifrar**; verifica prefijo `rsb1.`/`rsk1.`, máximo 16384 y ASCII seguro URL. Sin `root_handover`, borra paquete residual del espacio.
3. Nuevo titular obtiene `{blob, from_operator, created_at}` con `GET /api/superadmin/handover` (404 ausente), descifra/instala y rota: antigua firma `rsx1.`, publica nueva. Finalmente `DELETE /api/superadmin/handover`. CLI `remote-cli seal accept-handover`; Console muestra aceptación en panel si existe paquete.
4. **Rotar invalida copias antiguas.** Dispositivos online solo aceptan rotación firmada por raíz que ya confían. Antiguo titular no firma grants nuevos ni cambia raíz ya rotada. Antes de terminar, contraseña débil/vacía aún permite descifrar `rsb1.`; comunique contraseña separadamente.

**Otro canal:** `remote-cli seal handover <账号> --out-of-band` imprime solo una línea `rsb1.` y omite `root_handover`. Entregue línea/contraseña por otros medios; destinatario `remote-cli seal accept-handover <那一行>`. Cancelar contraseña Console (Android «Solo transferir cuenta») transfiere rol sin subir raíz; entréguela aparte como `rsk1.` o `rsb1.` externo.

## Negociación y formato de protocolo

```text
CLI                         Exchange                       agent
 │ open_sealed{session_id,    │                               │
 │   device_id, hello}        │──sealed_open{session_id,hello}▶│ 验证 hello 签名、授权 → 级别
 │                            │◀─sealed_accept{session_id,     │
 │◀─sealed_accept (原样转发) ──│        accept}                │
 │ 验证设备签名 + pin          │                               │
 │ sealed_input{packet} ─────▶│──sealed_input────────────────▶│ 解密、校验级别、执行
 │ 二进制帧 kind=4 (PTY 键入)─▶│── 只改写帧头编号 ─────────────▶│
 │◀── sealed_event{packet,last}│◀── sealed_event ──────────────│ 所有输出在出站队列加密
 │◀── 二进制帧 kind=4 ─────────│◀── 二进制帧 kind=4 ───────────│
```

- `hello` = `{v, operator_key, operator_ephemeral, nonce, grant?, sig}`; firma `["remote-seal-v1/hello", v, device_id, operator_key, operator_ephemeral, nonce, grant.sig|""]` (array JSON canónico).
- `rsr1.` firma `["remote-seal-v1/workspace-root", v, workspace_id, root_key, issued_at_ms]`; `rsx1.` = `{v, workspace_id, old_root, new_root, issued_at_ms, sig}`, `old_root` firma `["remote-seal-v1/root-rotation", v, workspace_id, old_root, new_root, issued_at_ms]`. Exchange→agent `{"type":"seal_root","token":"rsr1.…","rotation":"rsx1.…"?}` (solo control en claro desde Exchange, inválido en túnel).
- `accept` = `{v, device_key, device_ephemeral, nonce, level, sig}`; firma `["remote-seal-v1/accept", v, device_id, operator_key, operator_ephemeral, hello.nonce, hello.sig, device_key, device_ephemeral, nonce, level]`; alterar `level` invalida.
- Claves: `salt = SHA256(accept 签名字节)`, `PRK = HKDF-Extract(salt, X25519(o, b))`, `K_od = Expand("remote-seal-v1/operator_to_device")`, `K_do = Expand("remote-seal-v1/device_to_operator")`.
- AEAD ChaCha20-Poly1305, AAD `remote-seal-v1`; nonce byte canal (0 texto/1 binario) ‖ 3 ceros ‖ contador `u64` big-endian. Por dirección/canal empieza en 0, incrementa uno; discrepancia cierra.
- Texto `{"counter":n,"ciphertext":"<base64>"}`. JSON interno operador→dispositivo `SealedRequest` (`DeviceDown` existente o `seal_policy_get` / `seal_policy_set {mode?, root_key?, revoke?, unrevoke?}`); dispositivo→operador `SealedResponse` (`DeviceUp` o `seal_policy`). Agent reescribe `session_id` interno al túnel.
- Binario `StreamKind::Sealed = 4`, carga `counter(u64 BE) ‖ AEAD(inner_kind ‖ bytes)`; `inner_kind` 1/2/3 stdout/stderr/PTY.
- **Una** operación por sesión (exec, PTY, transferencia…), modelo 1:1. Agent cifra final y marca `last:true`; Exchange libera ruta/completa auditoría sin leer.
- `close_session` → `sealed_close{detach:false}` termina; desconexión operador → `sealed_close{detach:true}` (agent separa hold exec, termina otros).
- Máximo base64 unos 2.8 MiB para captura Android 2 MiB; envoltorio `MAX_SEALED_WIRE`.

Agent cifra o descarta texto en claro de IDs sellados; tras cerrar, ID queda aislado para siempre y salida hold tardía se descarta. Hold iniciado/adjuntado por sellado no aparece en lista antigua ni admite attach/tail/cat/kill antiguo.

## Exportación y copia de seguridad de raíz privada

Raíz controla todos los dispositivos. Superadministrador decide custodia; **Console guarda por defecto con contraseña vacía; borrar la copia desactiva el guardado automático**:

- **Exportar sin subir:** `rsk1.` + semilla 32 bytes base64url sin padding. CLI `seal root --export FILE|-` (0600); Console muestra campo Web o copia portapapeles Android. Quien la obtiene controla todo.
- **Copia cifrada (automática en Console, manual en CLI):** solo superadministrador cuya clave local es raíz publicada. Token versionado `rsb1.` JSON base64url máximo 4096 caracteres. Argon2id deriva contraseña, parámetros producción incluidos: 64 MiB, 3 iteraciones, paralelismo 1. Sal 16 bytes y nonce ChaCha20-Poly1305 12 bytes aleatorios por copia. AAD `remote-seal-v1/key-backup:{version}:{public_key}`: alterar versión/clave/cifrado falla; después comprueba clave derivada de semilla. Antes de Argon2 rechaza parámetros débiles/excesivos: memoria 8 KiB–256 MiB (producción mínimo 64 MiB), iteraciones 1–8 (producción mínimo 3), paralelismo solo 1.
- **Contraseña opcional, sin fuerza obligatoria.** Vacía también usa Argon2id y mismo formato. CLI/Web/Android advierten si vacía/débil (menos de 12 caracteres, pocas categorías, repetida/común): **Exchange o quien lea la base puede descifrar raíz y controlar todo**. Rust/JS comparten reglas/vectores, incluido vacío.

`PUT` / `GET` / `DELETE /api/seal/backup` sirven solo cuenta actual; `PUT`/`GET` requieren superadministrador. Cuerpo `{blob}`; Exchange comprueba prefijo `rsb1.`/longitud, **nunca descifra**. `GET` máximo 5 por cuenta/proceso/hora, luego 429. Solo frena ataques online; base copiada permite fuerza bruta offline. Auditoría lectura/escritura/borrado solo bytes.

- CLI `remote-cli seal backup` (contraseña oculta dos veces, Intro vacío o `--passphrase-stdin`); `remote-cli seal restore` con `--force` si hay otra clave, conserva `seal-identity.<毫秒>.previous.pk8`, fija raíz restaurada. También intenta al login. No imprime secretos/cifrado.
- Web requiere raíz exportable; al generar sustituye WebCrypto no exportable por semilla IndexedDB. Bootstrap restaura antes de identidad; `localStorage.seal.cloudBackup=off` desactiva.
- Android semilla expo-secure-store; `bootstrapSeal` restaura primero, `remote.seal.cloudBackup=off` desactiva.

## Uso

```bash
remote-cli seal show                       # 生成/显示本机操作员公钥与指纹
remote-cli exec lab-router -- uname -a
remote-cli shell lab-router
remote-cli seal policy lab-router          # 根：查看设备授权与吊销策略

# 根给同事手动授权（通常不需要：根客户端按角色自动签发）（默认 90 天；对方已登录的客户端会自动登记公钥）
remote-cli seal grant --device lab-router --to alice --level view
# 或仍用公钥；--no-publish 只打印 token
remote-cli seal grant --device lab-router --operator-key <KEY> --level view --no-publish
# 手动续签即将到期的已发布授权（默认 --before 30 --days 90；上线时也会自动续签）
remote-cli seal renew
# 同事：拉取 Exchange 上签给自己的授权（登录后也会自动 sync）
remote-cli seal sync
# 仍可手动导入
remote-cli seal import-grant rsg1.xxxxx

# 根私钥：导出（不上传）或可选的加密云端备份
remote-cli seal root --export root.rsk
remote-cli seal backup
remote-cli seal restore                    # 已有不同钥匙时加 --force

# 带外固定设备指纹（在设备上 `remote-agent seal show`）
remote-cli seal trust-device lab-router <DEVICE_KEY>
```

**Web Console / Android Console:** todas las operaciones de dispositivo utilizan exclusivamente sesiones selladas nativas. Comandos, lista de trabajos hold y kill/tail/cat, terminal, archivos, capturas y entrada de pantalla (Android/escritorio), árbol de interfaz, sensores, ubus, registros del sistema y actualización propia pasan por el túnel sellado; la página muestra «🔒 Cifrado de extremo a extremo». El panel «Cifrado de extremo a extremo» en los detalles Web o en Ajustes de Android equivale a la CLI: muestra clave/huella local y raíz del espacio (superadministrador: exportar/importar/rotar la raíz privada, recibir su traspaso y aprobar claves nuevas), consulta/modifica claves del dispositivo y política de revocación (requiere raíz), emite autorizaciones como tokens `rsg1.` que CLI importa mediante `seal import-grant`, importa autorizaciones, confía en claves públicas de dispositivos y ofrece copia automática de la raíz privada en la nube (contraseña vacía permitida, con advertencia expresa de que Exchange puede descifrarla si es débil). La primera conexión fija la huella (TOFU); los cambios posteriores se rechazan y exigen comprobarla. Los miembros normales tienen claves distintas en CLI, navegador y móvil, que se registran por separado y reciben autorización de la raíz según el rol. Para ejercer de raíz desde varios dispositivos, el superadministrador puede subir una copia cifrada y restaurarla automáticamente al acceder desde otro equipo, o importar la raíz privada en cada uno.

Local: `remote-agent seal show | root <key> | forget-root | revoke <key>`; reinicie agent tras cambios; `seal policy` sellado aplica inmediatamente.

## Comportamiento predeterminado

| Ámbito | Comportamiento actual |
|---|---|
| CLI, Web Console, Android Console | Todas las operaciones de dispositivo utilizan sesiones selladas nativas, sin interruptor para desactivarlas ni retorno a texto en claro |
| Política local del dispositivo | Todos los valores, incluido `allow`, rechazan operaciones sin cifrar |

- Los dispositivos nuevos fijan la raíz del espacio al conectarse a Exchange (TOFU); la raíz y sus autorizaciones permiten operar. Si no confía en Exchange durante la primera conexión, ejecute antes `remote-agent seal root <key>` en el dispositivo.
- Si la identidad no está disponible o la política está dañada, se bloquean las operaciones; repare las claves y la política en el dispositivo antes de volver a conectar.
