Saltar al contenido
miblo

SDK

Apps del Desk: declarar, ajustes, el supervisor y el sandbox

Convierte un programa en una app del Desk: declare, el esquema de ajustes, settings.get, request y onChange, la rotación, el supervisor y el sandbox de la comunidad.

Qué es una app

Una app es un programa que le dice a Miblo su nombre y los ajustes que necesita. La persona completa esos ajustes en Miblo Hub (pestaña Apps del Desk) o en la terminal, nunca en un archivo ni en una variable de entorno, y Miblo se encarga de ejecutar el programa. Las apps oficiales (miblo-apps) usan exactamente las rutas que usarás tú: no hay ninguna puerta privada.

Una app tiene tres partes:

  1. declarar la app (nombre, categoría, promesa, ajustes, una vista previa);

  2. leer sus ajustes y reaccionar cuando la persona cambia uno;

  3. dibujar su pantalla con una página web, la forma estándar desde Miblo 1.45 (apps del Desk de página), o con screen.card() y screen.layer(), la forma antigua y obsoleta (la pantalla de apps), o solo enviar alertas (apps solo de alertas).

Las apps necesitan Miblo 1.30 o más nuevo en el computador y en el Miblo Desk (el SDK 2.x); leer las sesiones de IA de la persona (sessions) necesita Miblo 1.34.

Una app del Desk de página

En manifest.json, una app del Desk de página dice "api": 5, "display": "page" y "page": "page.html" (un archivo .html de la carpeta). run() devuelve { data } (JSON, de hasta 16 KB) en vez de una tarjeta, y Miblo le entrega esos datos a la página. Necesita Miblo 1.45 (1.46 para api 5 y los perfiles de render) y no puede ser solo de alertas (screen: false). La guía completa, con lo que una página no puede hacer, es Apps del Desk de página.

app()

const app = miblo.app({ id: 'weather', tool: 'weather' });
  • id: el id de la app, letras minúsculas, dígitos y -, hasta 48 caracteres. Usa el mismo que miblo apps add --id.

  • tool: el nombre de tu programa (las reglas del tool de una sesión). Los ajustes de una app pertenecen al programa que la declaró, por ese nombre: otro programa recibe not_your_app. Una app instalada desde la tienda de apps del Desk pertenece al nombre del paquete.

declare()

await app.declare({
  name: { pt: 'Clima', en: 'Weather' },
  category: 'daily',
  promise: { pt: 'Vai chover?', en: 'Will it rain?' },
  settings: [
    { key: 'city', label: { pt: 'Cidade', en: 'City' }, type: 'text', required: true, max: 40 },
    { key: 'units', label: { pt: 'Unidade', en: 'Units' }, type: 'select', options: ['°C', '°F'], default: '°C' },
    { key: 'peek', label: { pt: 'Avisar antes da chuva', en: 'Heads-up before rain' }, type: 'toggle', default: true },
  ],
  preview: { v: 1, title: 'Weather', icon: 'cloud', items: [{ t: 'big', value: '24°', label: 'Recife' }] },
});
CampoReglas
nameUna cadena o { pt, en }, cortada a 24 caracteres. Obligatorio.
settingsEl esquema de ajustes (abajo), hasta 12.
categoryUn nombre corto para la pestaña Apps (a-z, 0-9, -, hasta 24). La tienda de apps del Desk usa computer, money, daily, news-dev, sport, delights y other.
promiseUna línea, { pt, en } o una cadena, cortada a 100 caracteres: la pregunta que responde la app.
previewUna tarjeta con datos de ejemplo, que se muestra en la pestaña Apps antes de que la app corra.
dwellLos segundos que sugiere en la pantalla de apps (5 a 120); gana la elección de la persona.
screenfalse: una app solo de alertas, sin pantalla.
alertsDe qué tratan sus alertas (una cadena o { pt, en }, 40 caracteres, que se muestra cuando se le pregunta a la persona), o sus tipos de alerta (apps solo de alertas).
sessionstrue: lee las sesiones de IA de la persona con app.sessions(), una vez que lo permite (Miblo 1.34).

Devuelve { ok, id }. Declarar de nuevo reemplaza el esquema y conserva todos los valores guardados que sigan siendo válidos con él: declara en cada inicio. El texto más largo que su límite se corta, no se rechaza. Como máximo 64 apps por computador (too_many_apps).

El esquema de ajustes

Cada ajuste es { key, label, type, default?, required? } más los campos de su tipo.

typeCampos extraValor
textmax (1 a 200, por defecto 100), suggest?, coords?Una línea de texto
numbermin?, max?Un número
selectoptions: de 1 a 2000 opciones, cada una una cadena o { value, label }; max?; dependsOn?Una de las opciones (una lista con max mayor que 1)
toggle(ninguno; por defecto false)true o false
url(ninguno)Una dirección http:// o https:// de hasta 512 caracteres
time(ninguno)Una hora HH:MM (24 h); acepta 22:00, 7:30 o 22h30 y siempre guarda HH:MM. Miblo Hub muestra un selector de hora
connectprovider, clientId, scopes, clientSecret?, help?La cuenta conectada (ver Conectar cuentas)
  • key: empieza con una letra minúscula, luego letras, dígitos y _, hasta 32 caracteres, única.

  • label: una cadena o { pt, en }, cortada a 40 caracteres.

  • required: true hace que la app aparezca como "necesita configuración" mientras no tenga valor.

  • default: debe ser en sí mismo un valor válido del tipo.

  • group: "advanced" pliega el ajuste bajo "Avançado" (Avanzado) en Miblo Hub, para los que casi nadie cambia.

  • Opciones de un select: un value tiene hasta 64 caracteres (sin repetidos), un label (una cadena o { pt, en }) se corta a 60; como máximo 6000 opciones en una app. Un max (1 a 20) mayor que 1 lo convierte en elección múltiple: el valor es una lista de como máximo max opciones distintas. dependsOn: '<key>' nombra un select de elección única anterior, y entonces options es un mapa de sus valores a listas ({ "bra.1": ["flamengo", ...] }).

  • Autocompletado (text): suggest: 'city' o 'free' recibe sugerencias que tu programa en ejecución responde con app.settings.onSuggest(key, async (q) => [{ value, label }]) (Python on_suggest); coords: true (solo con suggest: 'city') también acepta "lat,lon" desde "Usar mi ubicación" de Miblo Hub.

  • placeholder (text, number, url, time): un ejemplo de qué escribir, una cadena o { pt, en } de hasta 40 caracteres, que se muestra en el campo vacío. Donde exista una lista, declara un select (carga la lista desde su fuente y guárdala 7 días); texto libre solo donde no pueda existir una lista, y entonces con el ejemplo.

  • seed: un valor que tu programa trae de los ajustes de una versión anterior (un texto viejo dividido en ajustes nuevos). Miblo lo guarda una vez, solo donde la persona aún no tiene valor, nunca lo conserva en el esquema, e ignora uno que no sea válido. Un texto guardado cuyas partes son todas opciones de un select de elección múltiple que lo reemplazó se traslada por sí solo.

  • Sin secretos. Una app nunca le pide a la persona una clave, un token o una contraseña: no existe un tipo "password", y una clave o etiqueta que parezca un secreto (token, password, senha, api_key, secret, chave, cookie, pin...) se rechaza (bad_request). Las apps leen fuentes públicas, sin cuenta; para la cuenta propia de la persona, usa un ajuste connect (Conectar cuentas): Miblo la hace iniciar sesión con OAuth y tu app nunca ve una contraseña.

Leer los ajustes

settings.get()

const { values, missing, rev } = await app.settings.get();
  • values: los valores por defecto combinados con lo que la persona guardó; un ajuste sin ninguno de los dos es null.

  • missing: los ajustes obligatorios que aún no tienen valor.

  • rev: cambia cuando cambian los valores.

settings.request()

Le pregunta a la persona ahora, con un motivo (cortado a 80 caracteres).

if (missing.length) await app.settings.request(missing, 'To show the forecast');

Lo que pasa: Miblo Hub muestra una tarjeta de configuración con el formulario y el motivo, el celular dice "Configure o app Weather no computador", y la pantalla de la app en el Miblo muestra "Configure no Miblo Hub" en el lugar de su tarjeta. La solicitud se mantiene hasta que los valores cambian. Devuelve { ok, needsSetup }. El motivo no puede empezar como uno de los avisos de Miblo. Comparte el límite de las acciones del Miblo Desk (say, focus, timer: 3 a la vez, luego 1 cada 10 s, para todos los programas del computador juntos): pregunta una vez, no en un bucle.

settings.onChange() y settings.wait()

const stop = app.settings.onChange((values) => redraw(values));   // the person saved: redraw now
// ...
stop();
  • onChange(cb, { onError, retryMs }) llama a cb(values, answer) en cada cambio (no para los valores actuales) y devuelve stop(). Los errores van a onError y sigue intentando cada retryMs (por defecto 5000). En Python: on_change(cb, on_error=None, retry=5.0), con cb(values).

  • wait(rev?) es la pieza de debajo: espera a que los valores cambien respecto de rev, como máximo 20 s, y devuelve { values, missing, rev }. Como máximo 16 esperas a la vez (busy).

Los valores se quedan en este computador (<datos de Miblo>/apps/<id>.json, legible solo por tu usuario). Desde la terminal: miblo apps config <id> key=value.

Cuándo se muestra la pantalla de la app

  • La rotación: con varias apps, la pantalla de apps muestra una a la vez, cada una por su propio tiempo: miblo apps dwell <id> <seconds> (5 a 120; el valor por defecto es lo que la app sugiere cuando se declara, dwell, si no 15 s). Un tiempo por app; no hay uno global.

  • Cuándo: miblo apps when idle (por defecto: solo cuando ninguna sesión de IA ha estado corriendo, esperando ni necesitando a la persona durante --idle-after segundos, 60 por defecto, 10 a 3600), rotation (siempre en la rotación), request (solo cuando una app pide la pantalla con screen.request(), por su propio tiempo, mientras nada necesita a la persona) o manual (solo fijada o en un vistazo). La persona elige; una app no lo cambia.

  • Fijar y echar un vistazo siguen funcionando con cualquier elección; pedir la pantalla funciona con todas menos manual. Las alertas de Miblo siempre van primero.

  • Los límites son por computador, compartidos por todos los programas y las apps oficiales activadas: tarjetas 2 por segundo (ráfagas de 5), show 1 cada 10 s, vistazos 1 cada 10 s para todos (y 1 cada 10 minutos por programa). Mientras un programa tiene su pantalla fijada (screen.show(true)), el animate de otro programa recibe anim_taken con retryAfter.

El supervisor

miblo apps add "<command>" registra tu programa y miblo apps enable <id> lo mantiene corriendo: Miblo lo inicia con el token, lo reinicia cuando termina y guarda su salida (miblo apps logs <id>). El paso a paso está en Instalación.

ComandoQué hace
miblo apps list [--json]Todas las apps, encendidas o apagadas, y cuáles necesitan configuración
miblo apps enable / disable <id>La enciende o la apaga
miblo apps show <id>Estado, reinicios, comando o hosts
miblo apps logs <id>La salida del programa (y lo que bloqueó el sandbox)
miblo apps config <id> [key=value ...]Muestra o cambia los ajustes
miblo apps dwell [<id>|sessions|pet [<seconds>|default]]El tiempo de cada app en la pantalla (y el turno de las sesiones y de la mascota en la rotación)
miblo apps order [<id>|sessions|pet <position>]El orden de la rotación; --sessions-between on|off pone las sesiones entre dos apps
miblo apps when [rotation|idle|request|manual] [--idle-after <seconds>]Cuándo se muestran las apps
miblo apps pin [<id> [always|idle|after <minutes>|window <HH-HH>]] / apps unpinMantiene una app en la pantalla (siempre, en reposo, tras N minutos sin sesiones de IA, o entre dos horas)
miblo apps peek <id>Su pantalla en el Miblo ahora, por 10 s
miblo apps screen <id> on|offMuestra su pantalla o no (apagada: corre solo para sus alertas)
miblo apps alerts <id> [on|off|later]Permite sus alertas, las rechaza, o vuelve a preguntar en 7 días (apps solo de alertas)
miblo apps sessions <id> [on|off|later]Le permite ver las sesiones de IA (o no)
miblo apps connect|disconnect <id> <google|microsoft>Conecta u olvida una cuenta
miblo apps search [<words>] [--category <c>]Busca en la tienda de apps del Desk
miblo apps install <id>Instala una app de la tienda de apps del Desk (firmada)
miblo apps startInicia el puente y las apps que están encendidas (Miblo Hub lo hace al abrirse)
miblo apps remove <id>La apaga, le quita la pantalla y elimina sus ajustes
miblo screen status / clearMuestra o limpia la pantalla de apps

Solo tú, en tu propia terminal: miblo apps add, permitir las alertas de una app (apps alerts <id> on) o sus sesiones (apps sessions <id> on) se niegan a correr dentro de un agente de IA (Claude Code, Codex...). Escríbelos tú mismo, o usa Miblo Hub.

Conectar cuentas

Una app puede leer los datos propios de Google o Microsoft de la persona (la Agenda oficial lee su calendario así) sin ver nunca una contraseña ni un refresh token: el puente de Miblo hace iniciar sesión a la persona, con OAuth 2.0 (Authorization Code + PKCE, como una aplicación de escritorio), y tu programa solo recibe tokens de acceso de corta vida.

await app.declare({ name: 'Agenda', settings: [
  { key: 'google', label: { pt: 'Conectar Google', en: 'Connect Google' }, type: 'connect', provider: 'google',
    clientId: '<your desktop client id>.apps.googleusercontent.com', clientSecret: '<Google\'s installed-app secret>',
    scopes: ['https://www.googleapis.com/auth/calendar.readonly'],
    help: { pt: 'Os eventos aparecem no Miblo em até 30 s', en: 'Events show on Miblo within 30 s' } },
] });
const { url } = await app.connect.start('google');            // the sign-in address (the Miblo app has the button)
const { accessToken, expiresAt, account } = await app.connect.token('google');
await fetch('https://www.googleapis.com/calendar/v3/users/me/calendarList', { headers: { authorization: `Bearer ${accessToken}` } });
await app.connect.off('google');                              // disconnect
  • El ajuste connect dice dónde iniciar sesión: provider (google o microsoft, un ajuste cada uno), clientId (tu cliente OAuth: un cliente "Desktop app" de Google o un cliente público de Azure), scopes (1 a 8), el clientSecret de Google cuando tiene uno (Google lo emite para clientes de escritorio y lo documenta como no confidencial; nunca para Microsoft) y una línea help opcional (80 caracteres, bajo el botón). Nunca es obligatorio y no tiene valor por defecto; su valor es la cuenta conectada (el correo), '' hasta entonces, y solo lo escribe el puente. Así settings.wait() despierta a tu programa en el momento en que la persona conecta o desconecta.

  • connect.start(provider) devuelve { provider, url }, la dirección de inicio de sesión. El puente espera exactamente una redirección dentro de 5 minutos en un puerto loopback propio, verifica su state, intercambia el código con el verificador PKCE y verifica el id_token. El navegador dice "Conectado como ...". Como máximo 4 inicios de sesión a la vez (busy).

  • connect.token(provider) devuelve { provider, accessToken, expiresAt, account }, un token con al menos un minuto de vida, renovado cuando hace falta. not_connected hasta que la persona conecta (y de nuevo si el proveedor revoca el acceso); unavailable con retryAfter cuando no se puede llegar al proveedor: inténtalo más tarde, sigue conectada.

  • Sin ajuste connect para ese proveedor: start, token y off responden no_connect_setting (404).

  • connect.start y connect.off comparten el límite de las acciones del Miblo Desk (3, luego 1 cada 10 s); token es una lectura.

  • connect.off(provider) olvida la cuenta en este computador (miblo apps remove también).

  • Quién puede: solo el programa que declaró la app (not_your_app), como con los ajustes.

  • Dónde viven los tokens: el refresh token está sellado (AES-256-GCM, bajo una clave derivada de la clave del puente y ligada a la app y al proveedor) en <datos de Miblo>/apps/<id>/connect-<provider>.json; los tokens de acceso quedan solo en la memoria del puente. Nada va a miblo.ai.

  • Para la persona: la pestaña Apps del Desk de Miblo Hub muestra un botón "Conectar Google", luego "Conectado como ... · Desconectar". Desde una terminal: miblo apps connect <id> google y miblo apps disconnect <id> google.

Receta: suscriptores de YouTube

El número de suscriptores de un canal de YouTube, leído con la cuenta de Google de la propia persona (sin clave de API en la app):

  1. En la consola de Google Cloud, crea un proyecto, activa la YouTube Data API v3, configura la pantalla de consentimiento de OAuth y crea un cliente OAuth de tipo Desktop app. Copia su client id y su client secret (Google documenta el secreto de un cliente de escritorio como no confidencial).

  2. Declara un ajuste connect con el scope de solo lectura, y lee el conteo con el token de acceso que te entrega Miblo:

TypeScript
import { MibloStatus } from '@miblo/status';

const miblo = new MibloStatus();
const app = miblo.app({ id: 'youtube-subs', tool: 'YouTube' });
await app.declare({ name: 'YouTube', settings: [
  { key: 'youtube', label: { pt: 'Conectar YouTube', en: 'Connect YouTube' }, type: 'connect', provider: 'google',
    clientId: '<your client id>.apps.googleusercontent.com', clientSecret: '<your client secret>',
    scopes: ['https://www.googleapis.com/auth/youtube.readonly'] },
] });
const screen = miblo.screen({ tool: 'YouTube' });
for (;;) {
  try {
    const { accessToken } = await app.connect.token('google');
    const r = await fetch('https://www.googleapis.com/youtube/v3/channels?part=statistics&mine=true',
      { headers: { authorization: `Bearer ${accessToken}` } });
    if (!r.ok) throw new Error(`YouTube answered ${r.status}`);
    const subs = Number((await r.json()).items?.[0]?.statistics?.subscriberCount ?? 0);
    await screen.card({ v: 1, title: 'YouTube', items: [{ t: 'big', value: subs.toLocaleString('en-US'), label: 'subscribers' }] });
  } catch (e) {
    if (e.code === 'not_connected') { await app.settings.wait(); continue; }   // wakes when the person connects (20 s at most)
    console.error(e.code ?? e.message);
  }
  await new Promise((r) => setTimeout(r, 10 * 60_000));        // every 10 minutes
}

Luego la persona presiona Conectar YouTube en la pestaña Apps del Desk de Miblo Hub (o ejecuta miblo apps connect youtube-subs google).

  • Cuota: cada proyecto de Google Cloud recibe 10.000 unidades al día. channels.list y videos.list cuestan 1 unidad por llamada; search.list cuesta 100, así que evítala. Una lectura cada 10 minutos son 144 unidades al día.

El sandbox de las apps de la comunidad

Una app de la tienda de apps del Desk puede llevar código escrito por otra persona. Llega a un computador solo después del análisis automático, la revisión de IA y la aprobación de una persona (Publicar en la tienda de apps del Desk), y corre en un sandbox:

  • Archivos: lee solo su propio paquete, su propia carpeta de estado y el runtime de Miblo; escribe solo en su carpeta de estado. Usa el modelo de permisos de Node (--permission).

  • Sin procesos ni código nativo: sin procesos hijos, worker threads, addons nativos ni WASI.

  • Módulos bloqueados: http, https, http2, net, tls, dgram, dns, child_process, cluster, worker_threads, vm, module, wasi, inspector, repl, v8, trace_events y sqlite. La app importa solo archivos de su propio paquete y los demás módulos integrados de Node; el código que escribe en su carpeta de estado no se puede importar.

  • Red solo a través del fetch de Miblo, y solo a los hosts declarados en network: https en el puerto por defecto, las redirecciones se siguen bajo la misma regla, 10 s por llamada, 256 KB por respuesta, 30 llamadas por minuto. WebSocket, EventSource y XMLHttpRequest ya no están.

  • El token del SDK sale del entorno antes de que la app cargue; solo el cliente lo conserva.

  • Todo lo bloqueado se vuelve una línea blocked: ... en miblo apps logs <id>, incluso cuando la app captura el error.

  • La tienda de apps del Desk tiene la última palabra: en cada inicio y cada hora se verifica el paquete de nuevo; una app que la tienda de apps del Desk ya no sirve o que ocultó se apaga.

  • Actualizaciones (1.37.0): una versión más nueva cuyo acceso (hosts, sesiones, alertas) es el mismo se instala sola; una que pide más muestra "Actualización disponible" y espera a que la persona acepte (Miblo Hub, o miblo apps install <id> en una terminal).

  • Memoria y CPU (1.37.0): el heap de JavaScript de la app está limitado a 128 MB, y una que mantiene un núcleo de CPU ocupado durante 2 minutos se detiene (macOS y Linux); ambos cuentan como una falla y se inicia de nuevo más tarde.

  • Necesita Node 22.15+, 23.5+ o 24+. Miblo elige uno por sí solo: el propio Node del puente, si no MIBLO_NODE (la ruta a un node), si no uno en el PATH o en los lugares habituales (Homebrew, nvm, fnm, Volta...). Sin ninguno, la app no corre (estado unsupported, con el motivo en su log).

  • Encabezados: el fetch de Miblo envía lo que la app le da; los helpers ctx.fetchJson/fetchText aceptan solo authorization, accept, accept-language, if-none-match e if-modified-since (Publicar).

  • Errores y reintentos: cada falla es una línea en el log con el motivo (el campo de una tarjeta rechazada, el archivo y la línea del error, el estado HTTP con un extracto sin secretos), y la app lo intenta de nuevo en 5 s, 10, 20..., nunca más allá de su propio intervalMs (al menos 10 s después de un 4xx), para no gastar la cuota de una API.

Lo que el sandbox no garantiza: la red la sostienen el fetch reemplazado y el control de módulos, no el sistema operativo, y una app puede usar CPU y memoria y enviar a sus hosts lo que Node le da a cualquier programa (la hora, la configuración regional, el nombre del computador). Por eso una persona revisa primero cada versión.