SDK
Miblo apps: declaring, settings, the supervisor and the sandbox
Turn a program into a Miblo app: declare, the settings schema, settings.get, request and onChange, the rotation, the supervisor and the community apps' sandbox.
What an app is
An app is a program that tells Miblo its name and the settings it needs. The person fills those settings in the Miblo app (Apps tab) or the terminal, never in a file or an environment variable, and Miblo takes care of running the program. The official apps (miblo-apps) use exactly the routes you will use: there is no private door.
An app has three parts:
declaring the app (name, category, promise, settings, a preview);
reading its settings and reacting when the person changes one;
drawing its screen with
screen.card()orscreen.layer()(The App screen).
app()
const app = miblo.app({ id: 'weather', tool: 'weather' });app = miblo.app("weather", "weather")id: the app's id, lowercase letters, digits and-, up to 48 characters. Use the same one asmiblo apps add --id.tool: your program's name (a session'stoolrules). An app's settings belong to the program that declared it, by that name: another program getsnot_your_app. An app installed from the store belongs to the package's name.
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' }] },
});app.declare(
{"pt": "Clima", "en": "Weather"},
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},
],
category="daily",
promise={"pt": "Vai chover?", "en": "Will it rain?"},
preview={"v": 1, "title": "Weather", "icon": "cloud", "items": [{"t": "big", "value": "24°", "label": "Recife"}]},
)| Field | Rules |
|---|---|
name | A string or { pt, en }, up to 24 characters. Required. |
settings | The settings schema (below), up to 12. |
category | A short name for the Apps tab. The store uses computer, money, daily, news-dev, sport, delights and other. |
promise | One line, { pt, en } or a string, up to 100 characters: the question the app answers. |
preview | A card with sample data, shown in the Apps tab before the app runs. |
Returns { ok, id }. Declaring again replaces the schema and keeps every saved value still valid under it: declare on every start. At most 32 apps per computer (too_many_apps).
The settings schema
Each setting is { key, label, type, default?, required? } plus its type's fields.
type | Extra fields | Value |
|---|---|---|
text | max (1 to 200, default 100) | One line of text |
number | min?, max? | A number |
select | options: 1 to 20 choices (up to 40 characters each, no repeats) | One of the choices |
toggle | (none; default false) | true or false |
url | (none) | An http:// or https:// address of up to 512 characters |
connect | provider, clientId, scopes, clientSecret?, help? | The connected account (see Connecting accounts) |
key: starts with a lowercase letter, then letters, digits and_, up to 32 characters, unique.label: a string or{ pt, en }, up to 40 characters.required:truemakes the app show as "needs setting up" while it has no value.default: must itself be a valid value of the type.group:"advanced"folds the setting under "Avançado" (Advanced) in the Miblo app, for the ones almost nobody changes.No secrets. There is no "password" type, and a key or label that looks like a secret (
token,password,senha,api_key,secret,chave,cookie,pin...) is refused. Apps read public, account-free sources.
Reading the settings
settings.get()
const { values, missing, rev } = await app.settings.get();s = app.settings.get() # {"id", "values", "missing", "rev"}values: the defaults merged with what the person saved; a setting with neither isnull.missing: the required settings still without a value.rev: changes when the values do.
settings.request()
Asks the person now, with a reason of up to 80 characters.
if (missing.length) await app.settings.request(missing, 'To show the forecast');if s["missing"]:
app.settings.request(s["missing"], "To show the forecast")What happens: the Miblo app shows a setup card with the form and the reason, the phone says "Configure o app Weather no computador", and the app's screen on the Miblo shows "Configure no app Miblo" in its card's place. The request stands until the values change. Returns { ok, needsSetup }. The reason cannot start like one of Miblo's notices.
settings.onChange() and settings.wait()
const stop = app.settings.onChange((values) => redraw(values)); // the person saved: redraw now
// ...
stop();stop = app.settings.on_change(lambda values: redraw(values)) # on a background thread
# ...
stop()onChange(cb, { onError, retryMs })callscb(values, answer)on every change (not for the current values) and returnsstop(). Errors go toonErrorand it keeps trying everyretryMs(default 5000). In Python:on_change(cb, on_error=None, retry=5.0), withcb(values).wait(rev?)is the piece underneath: it waits for the values to change fromrev, at most 20 s, and returns{ values, missing, rev }. At most 16 waits at once (busy).
The values stay on this computer (<Miblo's data>/apps/<id>.json, readable by your user only). From the terminal: miblo apps config <id> key=value.
When the app's screen shows
The rotation: with several apps, the App screen shows one at a time,
miblo apps rotation <seconds>each (default 15;0: only the pinned one).When:
miblo apps when idle(the default: only when no AI session has been running, waiting or needing you for--idle-afterseconds, 60 by default, 10 to 3600),rotation(always in the rotation) ormanual(only pinned or in a peek). The person chooses; an app does not change it.Pin and peek still work with any choice. Miblo's alerts always come first.
The supervisor
miblo apps add "<command>" registers your program and miblo apps enable <id> keeps it running: Miblo starts it with the token, restarts it when it exits and keeps its output (miblo apps logs <id>). The step by step is in Install.
| Command | What it does |
|---|---|
miblo apps list [--json] | Every app, on or off, and which need setting up |
miblo apps enable / disable <id> | Turns it on or off |
miblo apps show <id> | State, restarts, command or hosts |
miblo apps logs <id> | The program's output (and what the sandbox blocked) |
miblo apps config <id> [key=value ...] | Shows or changes the settings |
miblo apps rotation [seconds] | Each screen's time in the rotation |
miblo apps when [rotation|idle|manual] | When the apps show |
miblo apps install <id> | Installs an app from the store (signed) |
miblo apps remove <id> | Turns it off, takes its screen away and deletes its settings |
miblo screen status / clear | Shows or clears the App screen |
Connecting accounts
With the next Miblo release, an app can read the person's own Google or Microsoft data (the official Agenda reads their calendar this way) without ever seeing a password or a refresh token: the Miblo bridge signs the person in, with OAuth 2.0 (Authorization Code + PKCE, as a desktop app), and your program only gets short-lived access tokens.
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'); // disconnecturl = app.connect.start("google")["url"]
token = app.connect.token("google")["accessToken"] # MibloError "not_connected" until the person connects
app.connect.off("google")The
connectsetting says where to sign in:provider(googleormicrosoft, one setting each),clientId(your OAuth client: a Google "Desktop app" client or an Azure public client),scopes(1 to 8), Google'sclientSecretwhen it has one (Google issues one to desktop clients and documents it as not confidential; never for Microsoft) and an optionalhelpline (80 characters, under the button). It is never required and has no default; its value is the connected account (the e-mail),''until then, and only the bridge writes it. Sosettings.wait()wakes your program the moment the person connects or disconnects.connect.start(provider)returns{ provider, url }, the sign-in address. The bridge waits for exactly one redirect within 5 minutes on a loopback port of its own, checks itsstate, exchanges the code with the PKCE verifier and checks the id_token. The browser says "Conectado como ...". At most 4 sign-ins at once (busy).connect.token(provider)returns{ provider, accessToken, expiresAt, account }, a token with at least a minute left, refreshed when needed.not_connecteduntil the person connects (and again if the provider revokes access);unavailablewithretryAfterwhen the provider cannot be reached: try later, still connected.connect.off(provider)forgets the account on this computer (miblo apps removedoes too).Who may: only the program that declared the app (
not_your_app), as with the settings.Where the tokens live: the refresh token is sealed (AES-256-GCM, under a key derived from the bridge key and bound to the app and provider) in
<Miblo's data>/apps/<id>/connect-<provider>.json; access tokens stay in the bridge's memory only. Nothing goes to miblo.ai.For the person: the Miblo app's Apps tab shows a "Conectar Google" button, then "Conectado como ... · Desconectar". From a terminal:
miblo apps connect <id> googleandmiblo apps disconnect <id> google.
The community apps' sandbox
An app from the store may carry code written by someone else. It reaches a computer only after the automatic scan, the AI review and a human's approval (Publish to the store), and runs in a sandbox:
Files: it reads only its own package, its own state folder and the Miblo runtime; it writes only its state folder. It uses Node's permission model (
--permission).No processes or native code: no child processes, worker threads, native addons or WASI.
Blocked modules:
http,https,http2,net,tls,dgram,dns,child_process,cluster,worker_threads,vm,module,wasi,inspector,repl,v8,trace_eventsandsqlite. The app imports only files of its own package and Node's other built-ins; code it writes to its state folder cannot be imported.Network only through Miblo's
fetch, and only to the hosts declared innetwork: https on the default port, redirects followed under the same rule, 10 s a call, 256 KB an answer, 30 calls a minute.WebSocket,EventSourceandXMLHttpRequestare gone.The SDK token leaves the environment before the app loads; only the client keeps it.
Everything blocked becomes a line
blocked: ...inmiblo apps logs <id>, even when the app catches the error.The store has the last word: at every start and every hour the package is checked again; an app the store no longer serves or has hidden is turned off.
It needs Node 22.15+, 23.5+ or 24+. On an older Node the app does not run (state
unsupported, with the reason in its log).
What the sandbox does not guarantee: the network is held by the replaced fetch and the module control, not by the operating system, and an app can use CPU and memory and send its hosts what Node gives any program (the time, the locale, the computer's name). That is why a human reviews every version first.