Los agentes de IA deben obtener https://remote.ivjn.us/SKILL.md.

# API HTTP para operaciones del Agent

> HTML: https://remote.ivjn.us/docs/es/http-api/

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. 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`.

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


HTTP `409` / `Sealed sessions are required.` en una acción de dispositivo exige una sesión sellada; un token Bearer por sí solo no basta. Use `remote-cli --profile alice exec lab -- uname -a`, o `remote-cli --profile alice --transport http exec lab -- uname -a` si solo se permite HTTP. La CLI mantiene el intercambio firmado y el cifrado de extremo a extremo.


## URL base

Exchange en producción:

```text
https://exchange.ivjn.us
```

Los despliegues propios utilizan su propio origen HTTPS. Todas las rutas siguientes son relativas a esa base. Envíe JSON con `Content-Type: application/json`.

## Autenticación

Todas las rutas `/api/*` (excepto las funciones públicas de autenticación/registro) requieren:

```http
Authorization: Bearer <token>
```

`<token>` puede ser:

1. **Token de sesión de acceso por contraseña** de `POST /api/auth/login`
2. **Clave API principal del operador** (el token permanente mostrado una sola vez al crear la cuenta de operador)

Ambos se aceptan mediante la misma ruta `authenticate_operator` utilizada por Console y CLI.

### Acceso por contraseña

```bash
curl -sS -X POST 'https://exchange.ivjn.us/api/auth/login' \
  -H 'Content-Type: application/json' \
  -d '{"email":"alice@example.com","password":"your-password-here"}'
```

`email` identifica la cuenta y debe ser una dirección válida. El servicio la normaliza (elimina espacios en los extremos y convierte a minúsculas) y exige que esté verificada. Los despliegues existentes pueden seguir enviando `{ "username": "alice", "password": "..." }` hasta migrar esas cuentas al correo; un `username` que contenga `@` se interpreta como correo. Una dirección no válida se rechaza antes de comprobar la contraseña.

Ejemplo de respuesta:

```json
{
  "username": "alice",
  "email": "alice@example.com",
  "tenant_id": "00000000-0000-0000-0000-000000000000",
  "token": "<prefix>.<secret>",
  "expires_in": 2592000
}
```

Utilice `token` como credencial Bearer. Llame a `POST /api/auth/logout` con la misma cabecera para revocar esa sesión. El cierre de sesión no revoca las claves API principales de operador.

### Clave API de operador (token principal)

```bash
TOKEN='<prefix>.<secret>'   # issued at operator creation; store offline
curl -sS 'https://exchange.ivjn.us/api/devices' \
  -H "Authorization: Bearer ${TOKEN}"
```

## Listar dispositivos

```bash
curl -sS 'https://exchange.ivjn.us/api/devices' \
  -H "Authorization: Bearer ${TOKEN}"
```

Cada elemento incluye `id`, `name`, `online`, `enabled`, `info`, marcas de tiempo y `active_sessions`. El inventario de dispositivos son metadatos de Exchange. Listar un dispositivo no establece la sesión sellada necesaria para operarlo.

## Rechazo HTTP de acciones de dispositivo y administración de sesiones

Todas las operaciones de dispositivo utilizan sesiones WebSocket selladas. Las rutas HTTP siguientes no pueden establecer la negociación entre cliente y dispositivo y devuelven HTTP `409` para solicitudes autenticadas, autorizadas y válidas. El rechazo se produce antes de enviarlas al dispositivo: no se envía al Agent ningún comando, solicitud de sensores, sondeo de red, actualización, llamada ubus, solicitud de trabajos retenidos ni comando predefinido. Que el dispositivo esté conectado no permite ejecutar estas rutas.

Se siguen aplicando autenticación, permisos y validación de entrada. Las credenciales no válidas devuelven `401`; las operaciones prohibidas, `403`; los comandos, URL o cuerpos no válidos conservan sus errores de validación, incluido `400`. El `409` de sesión sellada no sustituye errores previos de validación o autorización. Los permisos de comandos predefinidos siguen su propia política; superarlos tampoco permite ejecutarlos mediante HTTP.

| Método y ruta | Comportamiento tras autenticación, autorización y validación |
| --- | --- |
| `POST /api/devices/{id}/exec` | `409`: ejecutar dentro de una sesión WebSocket sellada |
| `GET /api/devices/{id}/exec/holds` | `409`: consultar trabajos retenidos dentro de una sesión WebSocket sellada |
| `POST /api/devices/{id}/exec/holds/{job_id}/kill` | `409`: detener trabajos retenidos dentro de una sesión WebSocket sellada |
| `GET /api/devices/{id}/exec/holds/{job_id}/cat` | `409`: leer salida de trabajos retenidos dentro de una sesión WebSocket sellada |
| `POST /api/devices/{id}/sensors` | `409`: recopilar sensores dentro de una sesión WebSocket sellada |
| `POST /api/devices/{id}/net` | `409`: realizar sondeos de red dentro de una sesión WebSocket sellada |
| `POST /api/devices/{id}/update` | `409`: comprobar o aplicar actualizaciones dentro de una sesión WebSocket sellada |
| `POST /api/devices/{id}/ubus` | `409`: llamar a ubus dentro de una sesión WebSocket sellada |
| `POST /api/devices/{id}/commands/{cmd}/run` | `409`: obtener el comando predefinido y ejecutarlo dentro de una sesión WebSocket sellada con una autorización de administración aceptada por el dispositivo |

Por ejemplo, esta solicitud de acción válida comprueba el rechazo; no es una interfaz de ejecución de comandos:

```bash
curl -i -X POST "https://exchange.ivjn.us/api/devices/${DEVICE_ID}/exec" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{"command":"uname -a","timeout_secs":30}'
```

Se espera HTTP `409` con el mensaje de error en inglés `Sealed sessions are required.` (localizado según el idioma de la solicitud). Establezca una sesión WebSocket sellada nativa para realizar la operación. Esta ruta HTTP no devuelve una respuesta de éxito con la salida del comando.

### Administración de sesiones de Exchange

El inventario y la cancelación de sesiones siguen disponibles mediante HTTP porque administran metadatos de enrutamiento de Exchange. No ejecutan operaciones de dispositivo ni abren una sesión adicional en él.

| Método y ruta | Cuerpo de éxito |
| --- | --- |
| `GET /api/devices/{id}/sessions` | `{sessions:[DeviceSessionInfo], limit, count}` |
| `POST /api/devices/{id}/sessions/{session_id}/kill` | `{session_id, released}` |

Los campos de `DeviceSessionInfo` son `session_id` (ID numérico del lado del dispositivo), `operator_session_id`, `kind`, `hold`, `age_secs` y `deadline_unix_ms`. `limit` es `max_sessions_per_device` (32 por defecto); `count` es la longitud de la lista actual. El listado lee directamente el estado del hub local de Exchange; un dispositivo desconectado localmente devuelve `503`. La cancelación utiliza el ID **del lado del dispositivo**, envía la cancelación correspondiente (incluido exec retenido) y libera localmente la ruta aunque el dispositivo no responda. Una sesión inexistente o un dispositivo desconectado localmente devuelve HTTP `200` con `released:false`. Los operadores de solo lectura reciben `403` en ambas interfaces.

```bash
curl -sS "https://exchange.ivjn.us/api/devices/${DEVICE_ID}/sessions" \
  -H "Authorization: Bearer ${TOKEN}"

curl -sS -X POST "https://exchange.ivjn.us/api/devices/${DEVICE_ID}/sessions/${SESSION_ID}/kill" \
  -H "Authorization: Bearer ${TOKEN}"
```

Antes de rechazar solicitudes por el límite de sesiones, Exchange recupera las rutas de operadores desaparecidos y aquellas cuyo plazo venció: plazo de exec + 15 segundos, transferencias de archivos 3600 segundos + 15 segundos, otros tipos 24 horas. Un exec retenido dentro del plazo se desvincula al desconectar el operador en lugar de terminarse.

### Operaciones CLI selladas nativas

La CLI establece sesiones selladas como parte de las operaciones normales de dispositivo. Complete primero la aprobación de la raíz del espacio y la autorización del dispositivo; consulte [sesiones selladas](/docs/es/sealed-sessions.md). No existe una opción de acceso en claro. Los comandos normales no necesitan parámetros adicionales de sellado:

```bash
remote-cli exec "$DEVICE_ID" -- uname -a
remote-cli shell "$DEVICE_ID"
remote-cli exec-holds "$DEVICE_ID"
remote-cli exec-hold-cat "$DEVICE_ID" "$JOB_ID"
remote-cli exec-hold-kill "$DEVICE_ID" "$JOB_ID"
remote-cli sensors "$DEVICE_ID" --json
remote-cli net "$DEVICE_ID" ping 1.1.1.1
remote-cli net "$DEVICE_ID" http https://example.com/
remote-cli ubus "$DEVICE_ID" system board '{}'
remote-cli commands list "$DEVICE_ID"
remote-cli commands run "$DEVICE_ID" "$COMMAND_ID"
```

Las operaciones de trabajos retenidos se ejecutan en sesiones selladas; la administración de sesiones de Exchange está disponible por separado mediante `remote-cli sessions` y `remote-cli session-kill`. Obtener metadatos de comandos predefinidos no autoriza su ejecución: el dispositivo debe aceptar la autorización de administración del operador sellado.

## Directorio de claves de sesiones selladas y retransmisión de autorizaciones

Exchange almacena claves públicas de operadores y tokens de autorización `rsg1.` firmados por el propietario para que los clientes se descubran. **Esto no constituye autorización.** Los dispositivos siguen verificando la firma del propietario según su política local. Exchange solo indexa la carga del token (`owner_key`, `operator_key`, `device_id`, `level`, `expires_at`) y nunca considera que la carga otorgue permisos. Exchange no puede falsificar firmas raíz, pero puede ocultar credenciales, falsear la lista de miembros ante un cliente raíz que apruebe automáticamente, observar metadatos o denegar el servicio.

La raíz del espacio (`rsr1.`) y las invitaciones de autorización (`rsi1.`) utilizan el mismo tipo de retransmisión. Exchange comprueba el formato del token y la autofirma Ed25519 (la invitación también debe coincidir con la raíz almacenada) y **no** decide qué claves de operador están aprobadas. `GET /api/seal/roster` ofrece al superadministrador una vista de miembros, roles y `operator_device_grants`. Los clientes no deben confiar en una clave recién aparecida hasta que el cliente raíz la apruebe localmente. Consulte `docs/sealed-sessions.md`.

Cualquier miembro autenticado puede listar claves, registrar su propia clave y cargar autorizaciones (que se autentican por sí mismas). El `DELETE` de una clave se limita a la cuenta que la registró (los administradores pueden borrar cualquiera). El `DELETE` de una autorización se limita a quien la cargó (los administradores pueden borrar cualquiera). `GET /api/seal/grants` omite registros vencidos.

La prueba de registro de clave es una firma Ed25519 de:

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

`issued_at_ms` debe estar dentro de ±10 minutos del reloj del servidor. `public_key` y `proof` utilizan base64 estándar (clave de 32 bytes y firma de 64 bytes).

| Método y ruta | Cuerpo | Éxito |
| --- | --- | --- |
| `POST /api/seal/keys` | `{"public_key","label"?,"proof","issued_at_ms"}` | Clave registrada (inserción o actualización para la misma cuenta) |
| `GET /api/seal/keys` | — | `[{operator_id, username, email, public_key, label, created_at, last_seen_at}]` |
| `DELETE /api/seal/keys/{key}` | — | `204` (`{key}` es la clave pública codificada para URL) |
| `POST /api/seal/grants` | `{"tokens":["rsg1.…"]}` | `{stored:[…], errors:[…]}` |
| `GET /api/seal/grants?mine=1` | — | Autorizaciones cuyo `operator_key` sea una de las claves registradas del solicitante. Cada autorización cuyo `owner_key` no sea la raíz publicada también incluye `chain` (tokens de certificados, posiblemente vacío) |
| `GET /api/seal/grants?owner_key=…` | — | Autorizaciones firmadas por esa clave de propietario |
| `DELETE /api/seal/grants/{id}` | — | `204` |
| `PUT /api/seal/root` | `{"token":"rsr1.…","rotation":"rsx1.…"?}` | Raíz del espacio almacenada. Solo superadministrador. Se comprueban formato y autofirma; esto no constituye aprobación. `rotation` (opcional) debe estar firmada por la raíz publicada actual e identificar la nueva; Exchange envía `seal_root {token, rotation}` a los dispositivos sellados conectados, que cambian su raíz fijada solo si la rotación está firmada por la raíz en la que ya confían. Sin rotación válida, los dispositivos que fijaron la raíz anterior la conservan |
| `GET /api/seal/root` | — | `{workspace_id, root_key, payload, signature, token, created_at}`, o `404` si no hay raíz publicada |
| `POST /api/seal/invitations` | `{"token":"rsi1.…"}` | Invitación almacenada. Solo superadministrador y solo si ya existe una raíz |
| `GET /api/seal/invitations` | — | Invitaciones no vencidas, más recientes primero (`[]` si no hay ninguna) |
| `GET /api/seal/roster` | — | `{members:[{operator_id, username, email, role, restricted, enabled, keys:[{public_key, label}], device_grants:[{device_id, can_manage}]}]}`. Solo superadministrador. Incluye miembros deshabilitados |

La indisponibilidad del directorio no elimina los requisitos de sesión sellada y autorización del dispositivo. `GET /api/seal/root` devuelve `404` si no se ha publicado una raíz del espacio.

```bash
# Register this operator key (proof signed locally)
curl -sS -X POST "$BASE/api/seal/keys" \
  -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"public_key":"<ed25519-b64>","label":"cli","proof":"<sig-b64>","issued_at_ms":1700000000000}'

curl -sS "$BASE/api/seal/keys" -H "Authorization: Bearer ${TOKEN}"

curl -sS -X POST "$BASE/api/seal/grants" \
  -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"tokens":["rsg1.…"]}'

curl -sS "$BASE/api/seal/grants?mine=1" -H "Authorization: Bearer ${TOKEN}"
```

### Certificados intermedios (`rsc1.`)

Exchange almacena certificados intermedios para que una clave no raíz pueda firmar autorizaciones aceptadas por los dispositivos. **Verifica** la firma Ed25519 del emisor y **no puede falsificar** un certificado. Cargar un token no constituye autorización: el solicitante debe ser el operador emisor. Un certificado firmado por la raíz (`issuer_key` coincide con la raíz publicada) solo puede publicarlo el superadministrador del espacio. Un intermedio solo puede publicarlo la cuenta cuya clave sellada registrada sea `issuer_key`; además, ese emisor debe ser el sujeto de un certificado almacenado, no revocado, no vencido y con `can_issue`. El hijo debe respetar el nivel, alcance de dispositivos y vencimiento del padre (las mismas comprobaciones que aplican los dispositivos). Volver a publicar el mismo par `(issuer_key, subject_key)` sustituye el registro anterior y elimina su revocación.

`GET /api/seal/certs/chain?subject_key=` devuelve `{chain:[tokens]}` siguiendo los enlaces de emisor hasta la raíz publicada, como máximo 4 certificados, primero el emitido por la raíz, o `404`. Puede llamarlo cualquier miembro autenticado. `GET /api/seal/grants?mine=1` añade esa `chain` a las autorizaciones cuyo firmante no sea la raíz para entregarla junto con ellas. Las firmadas por la raíz omiten `chain`.

`GET /api/seal/certs` lista todos los registros, incluidos los revocados (administradores y superadministrador). `DELETE /api/seal/certs/{serial}` (cuenta del emisor o superadministrador) establece `revoked`. Esto solo detiene su distribución. Los dispositivos siguen aceptando las copias que ya poseen hasta su vencimiento; la revocación efectiva se configura en `revoked_keys` de la política del dispositivo. `{serial}` es el número de serie base64 estándar codificado para URL.

| Método y ruta | Cuerpo | Éxito |
| --- | --- | --- |
| `POST /api/seal/certs` | `{"token":"rsc1.…"}` | Certificado almacenado (inserción o actualización por emisor y sujeto) |
| `GET /api/seal/certs/chain?subject_key=` | — | `{chain:["rsc1.…"]}`, primero el emitido por la raíz, o `404` |
| `GET /api/seal/certs` | — | `[{serial, issuer_key, subject_key, token, expires_at_ms, created_at, revoked}]`. Administrador o superadministrador |
| `DELETE /api/seal/certs/{serial}` | — | `204`. Emisor o superadministrador. Solo detiene la distribución |

```bash
curl -sS -X POST "$BASE/api/seal/certs" \
  -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"token":"rsc1.…"}'

curl -sS "$BASE/api/seal/certs/chain?subject_key=<ed25519-b64>" \
  -H "Authorization: Bearer ${TOKEN}"
```

### Copia de seguridad de la clave raíz del espacio (opcional)

`PUT` / `GET` / `DELETE /api/seal/backup` almacenan un bloque opaco `rsb1.` por cuenta. `PUT` y `GET` son exclusivos del superadministrador: el bloque es la clave privada **raíz** del espacio, cifrada en el cliente con Argon2id (64 MiB, t=3, p=1) y ChaCha20-Poly1305. La frase de contraseña es opcional y no se exige fortaleza mínima. **Con una frase vacía o débil, cualquiera que posea el bloque (incluido Exchange) puede descifrar la clave raíz y controlar todos los dispositivos.** La carga es opcional; los clientes pueden exportar la clave raíz (`rsk1.`) y conservarla fuera de línea. Exchange solo comprueba que `blob` comience por `rsb1.` y su longitud; nunca lo descifra. `GET` permite 5 solicitudes por cuenta, hora y proceso (HTTP 429). Las lecturas, escrituras y eliminaciones se auditan únicamente con el número de bytes. Las respuestas incluyen `Cache-Control: no-store`.

| Método y ruta | Cuerpo | Éxito |
| --- | --- | --- |
| `PUT /api/seal/backup` | `{"blob":"rsb1.…"}` | `{blob, updated_at}` |
| `GET /api/seal/backup` | — | `{blob, updated_at}`, o `404` si no existe. Limitado por frecuencia |
| `DELETE /api/seal/backup` | — | `204`, también si no existe el registro |

```bash
curl -sS -X PUT "$BASE/api/seal/backup" \
  -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"blob":"rsb1.…"}'

curl -sS "$BASE/api/seal/backup" -H "Authorization: Bearer ${TOKEN}"

curl -sS -X DELETE "$BASE/api/seal/backup" -H "Authorization: Bearer ${TOKEN}"
```

## Autenticación de la extensión de navegador mediante la cadena raíz

Las operaciones de navegador utilizan sesiones WebSocket selladas nativas, en lugar de ejecución HTTP individual. La extensión genera su propia clave de operador Ed25519. Con un token Bearer de operador obtiene `tenant_id` / `operator_id` de `/api/auth/me` y llama a `POST /api/seal/keys` con `{public_key, label, proof, issued_at_ms}`. La prueba de posesión firma:

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

La clave/firma utilizan base64 estándar (32/64 bytes); la hora debe estar dentro de ±10 minutos del reloj del servidor. El registro demuestra posesión, no autorización. La extensión sincroniza `GET /api/seal/root`, `GET /api/seal/grants?mine=1` y los tokens de certificados incluidos con las autorizaciones u obtenidos de `GET /api/seal/certs/chain?subject_key=<url-encoded-key>`. Las autorizaciones deben verificarse hasta la raíz de confianza del espacio, directamente o mediante una cadena válida, con alcance de dispositivo, nivel y vigencia correctos. El registro con token de dispositivo sigue siendo independiente de este flujo de directorio con token de operador.

La extensión verifica los saludos nativos entrantes mediante su raíz e incluye su propio saludo/autorización/cadena firmados en `SealAccept.peer`. CLI/Console verifican esa prueba y exigen `peer.operator_key == accept.device_key` antes de aceptar la respuesta firmada. Los Agents Rust utilizan su política raíz habitual cuando la extensión actúa como operador. Las pruebas inválidas o ausentes bloquean la operación; no existe otro almacén/interfaz de confianza de navegador ni compatibilidad alternativa antigua. Se conserva TOFU para dispositivos nativos y raíces del espacio: la extensión guarda la primera raíz de cada servidor/espacio y rechaza cambios.

**Seguridad:** verifique la primera raíz por un canal independiente; la prueba de posesión o un rol de servidor no equivalen a aprobación de la raíz. Revise las claves desconocidas antes de la aprobación automática del cliente raíz. Proteja los perfiles/claves de los extremos y los scripts hospedados. Eliminar entradas del directorio no revoca credenciales firmadas en caché. Exchange puede ver metadatos, ocultar credenciales o denegar servicio; el sellado no aísla a un operador hostil con acceso CDP completo.

## Elegir WebSocket o HTTPS

| Necesidad | Utilice |
| --- | --- |
| Comandos, consulta o cancelación de trabajos retenidos, sensores, sondeos de red, actualizaciones, ubus, comandos predefinidos | Sesión WebSocket sellada nativa |
| PTY interactivo, salida continua, transferencia de archivos, pantalla/árbol/pulsaciones Android | Sesión WebSocket sellada nativa |
| Autenticación, inventario de dispositivos, directorio de claves/autorizaciones/certificados, copia de la raíz | API HTTPS |
| Inventario y cancelación de sesiones de enrutamiento de Exchange | Interfaces HTTPS de administración de sesiones |
| Presencia de dispositivos en tiempo real y conexión persistente de operador | WebSocket; las operaciones de dispositivo en la conexión siguen requiriendo sellado |

Flujo de tickets del navegador (las credenciales nunca van en la URL WebSocket):

1. `POST /api/ws-ticket` con `Authorization: Bearer …`.
2. Conectarse a `GET /ws/browser` y enviar la primera trama JSON `{"ticket":"…"}`.
3. Establecer la negociación sellada nativa entre cliente y dispositivo antes de enviar cualquier operación. La autorización y la cadena de certificados del operador deben verificarse hasta la raíz en la que confía el dispositivo.

Los clientes nativos utilizan `GET /ws/operator` con autenticación de operador y el mismo requisito de sesiones selladas. Una conexión WebSocket o un acceso correcto a la cuenta no autorizan por sí solos operaciones de dispositivo. Consulte [Consola web y Web Push](/docs/es/web-console-and-notifications.md), [protocolo](/docs/es/websocket-protocol.md) y [sesiones selladas](/docs/es/sealed-sessions.md).

## Referencia rápida de estados de error

| Estado | Causa habitual |
| --- | --- |
| `400` | Entrada de acción no válida, como comando vacío o URL de actualización incorrecta; se valida antes del rechazo de sesión sellada |
| `401` | Token Bearer ausente o no válido |
| `403` | Los permisos de cuenta o dispositivo no autorizan la solicitud |
| `409` | La acción HTTP válida y autorizada requiere una sesión WebSocket sellada; no se envía al dispositivo |
| `404` | Entrada de directorio ausente o raíz del espacio no publicada, según el contrato de la interfaz |
| `429` | Límite de frecuencia de la interfaz, incluidas lecturas de copias de la raíz |
| `503` | El inventario de sesiones no encuentra el dispositivo en el hub local de Exchange |

Las rutas HTTP de acciones no esperan la salida del Agent; su resultado autorizado normal es `409`, no éxito de ejecución, error de envío por desconexión ni vencimiento de ejecución HTTP. Los errores de las API de metadatos, autenticación y directorio conservan sus significados particulares.

## Plan de validación manual

Utilice una cuenta y un dispositivo de prueba con permisos conocidos. Las comprobaciones de rechazo HTTP no necesitan ejecutar trabajo en el dispositivo:

1. Autentíquese y liste dispositivos mediante HTTPS; verifique los permisos de cuenta y dispositivo.
2. Envíe una solicitud válida a cada ruta de acciones de la tabla de rechazo. Espere HTTP `409` y confirme que ninguna acción llega al Agent, incluidas lecturas de trabajos retenidos y ejecuciones de comandos predefinidos.
3. Compruebe por separado credenciales inválidas, cuentas sin permisos y entradas no válidas. Espere los errores de autenticación, permisos y validación correspondientes, sin suponer que toda solicitud devuelve `409`.
4. Consulte el inventario de sesiones de Exchange. Para una sesión desechable seleccionada expresamente, pruebe la cancelación y verifique `released`; una sesión inexistente devuelve `released:false`. Listar sesiones de un dispositivo desconectado localmente devuelve `503`.
5. Complete la aprobación raíz y las autorizaciones y ejecute una operación CLI nativa inocua, como `remote-cli exec "$DEVICE_ID" -- uname -a`. Verifique que funciona mediante la negociación sellada. Una autorización ausente o inválida debe bloquear la operación.
6. Valide por separado el registro de claves, la obtención de autorizaciones/certificados, la copia opcional de la raíz y la autenticación del navegador mediante cadena raíz; esas API HTTPS siguen disponibles según los contratos anteriores.

## Destinos de exportación de auditoría

Solo Exchange PostgreSQL, aislado por espacio autenticado. Exclusivo para administradores y superadministradores; otras cuentas reciben 403. exchange-worker no lo admite; sin interfaz de configuración en Console Web/móvil.

```text
GET    /api/audit/sinks
POST   /api/audit/sinks
PATCH  /api/audit/sinks/{id}
DELETE /api/audit/sinks/{id}
POST   /api/audit/sinks/{id}/test
GET    /api/audit/sinks/status
```

```sh
printf '%s' '{"name":"archive","kind":"webhook","endpoint":"https://audit.example.net/events","secret":"REPLACE-WITH-RANDOM-SECRET","mode":"full"}' | remote-cli audit sink add
remote-cli audit sink list
printf '%s' '{"interval_ms":500,"batch_size":16}' | remote-cli audit sink update 1
remote-cli audit sink test 1
remote-cli audit sink status
remote-cli audit sink remove 1
```

Por defecto throttled envía los registros almacenados: texto superior a 256 KiB conserva los primeros 250 KiB y últimos 6 KiB, archivos solo como hash y flujos al terminar la sesión. full captura antes de truncar y divide JSON original y binarios en bloques (1 MiB por defecto), con ID de operación/objeto, secuencia, desplazamiento, marca final, SHA-256 y tamaño total. En sealed ambos modos envían solo metadatos y referencias/hashes del cifrado; no recuperan texto plano del dispositivo.

Secretos y cabeceras se almacenan cifrados y se muestran como ***. PATCH conserva ***; null elimina una cabecera. Colas FIFO persistentes asíncronas con reintentos exponenciales. La cola throttled llena elimina y cuenta los registros más antiguos; la cola full en disco aplica contrapresión. Status muestra éxito/fallo, pendientes, dropped y gaps. Eliminar descarta pendientes y audita el número; los cambios se auditan en la misma transacción.

Webhooks usan un array JSON events con bytes base64. Verifique X-Remote-Timestamp y X-Remote-Signature: sha256=<hex> con HMAC-SHA256(timestamp + "." + cuerpo original). OTel usa OTLP/HTTP JSON /v1/logs, body.bytesValue para bloques, atributos remote.device.id / remote.operator / remote.action y service.name configurable. gRPC requiere un Collector; no hay exportación directa.

A continuación se muestran configuración predeterminada, carga webhook, verificación y Collector. Verifique el cuerpo HTTP original; persista y deduplique por delivery_id antes de responder 2xx. Active allow_structured_metadata en Loki 3.x, añada una fuente Loki en Grafana y consulte {service_name="remote-exchange"}. Para gRPC cambie pipeline exporters a [otlp/grpc]. Use TLS y autenticación en producción.

```json
{"name":"collector","kind":"otel","mode":"throttled","endpoint":"https://collector.example.net:4318/v1/logs","headers":{"Authorization":"Bearer REPLACE"},"service_name":"remote-exchange","resource":{"deployment.environment.name":"production"},"batch_size":32,"interval_ms":1000,"chunk_bytes":1048576,"queue_limit":10000,"overflow_bytes":10737418240,"actions":[],"devices":[],"operators":[],"enabled":true,"ca_pem":null}
```

```json
{"version":1,"events":[{"delivery_id":123,"type":"full","mode":"full","entry":{"id":42,"action":"exec","operator_name":"alice","device_id":"dev"},"data":{"kind":"binary","asset_id":"asset-uuid","path":"/data_base64","sequence":0,"offset":0,"last":true,"data_base64":"YWJj","total_size":3,"sha256":"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad","context":{"direction":"output","frame_sequence":0,"stream_offset":0}}}]}
```

```python
import hashlib, hmac, time

def verify(headers, raw_body, secret):
    stamp = headers['X-Remote-Timestamp']
    if abs(time.time() - int(stamp)) > 300:
        raise ValueError('expired webhook')
    expected = 'sha256=' + hmac.new(
        secret.encode(), stamp.encode() + b'.' + raw_body, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(expected, headers['X-Remote-Signature']):
        raise ValueError('invalid signature')
```

```yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 127.0.0.1:4318
processors:
  batch: {}
exporters:
  otlphttp/loki:
    endpoint: http://loki:3100/otlp
  otlp/grpc:
    endpoint: telemetry.example.net:4317
    headers:
      Authorization: "${env:OTEL_AUTHORIZATION}"
    tls:
      insecure: false
service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp/loki]
```

| Campo de auditoría | Campo OTLP |
|---|---|
| service_name / resource | Resource service.name / attributes |
| entry.started_at | timeUnixNano |
| entry.id / delivery_id | remote.audit.id / remote.delivery.id |
| entry.device_id / operator_name / action | remote.device.id / remote.operator / remote.action |
| entry.tenant_id | remote.workspace.id |
| entry.started_at / ended_at / detail | remote.started_at / remote.ended_at / remote.result |
| traceId / spanId | traceId / spanId |
| data.data_base64 | body.bytesValue / remote.chunk.* |
| stored events | body.stringValue |
