Hanabi Developer Hub
/ Help / Limits & quotas
Help

Limits & quotas

What modules can and cannot do, and the quotas on files, workers, jobs, and data.

Modules can
read / write their own files
run a Python worker
store JSON
show notifications
fetch declared origins
Modules cannot
use localStorage
reach the network without permission
read other modules' files
run arbitrary server code
open Desktop files without consent
The sandbox boundary at a glance.

This page is the honest boundary for module authors. It separates hard limits (enforced or impossible), current limits (not built yet), and soft limits (quotas/conventions). Tags: [enforced] today, [planned] by design, [gap] a known missing piece being built.

What a module can do #

  • Render any HTML/CSS/JS UI in a resizable window sized by its manifest.
  • Ship at any size — module packages and their files have no artificial size limit (path-safety and the secret/executable blocks still apply). [enforced]
  • Optionally request broad read of the user's workspace (files.read.all, consent-gated) so a module can open files from Desktop/Documents/etc. — never another module's folder. [live]: consent prompt at install + a fallback prompt on first use, a host file-open dialog (pickUserFile), and scope enforcement. See examples/modules/broad-reader.
  • Read files the user explicitly picks from its own input folders. [live]
  • Write generated files into its own output folder. [live]
  • Organize its own folders — list/stat, make subfolders, and rename/move/copy/delete its own files and folders via hanabi.fs.* (files.manage; deletes go to Trash, never a permanent wipe). Widens to the whole workspace with the consent-gated files.write.all, never another module's folders. [live]
  • Integrate with the user's Desktop (the consent-gated desktop.* namespace): read what's on the Desktop (desktop.read), place its own files/folders there (desktop.workspace), pin a launcher shortcut that reopens it (desktop.shortcuts), or set the wallpaper / accent / theme (desktop.personalize). The Desktop is the user's surface, so each needs consent and a module can only touch what it placed and pick built-in appearance values. [live]
  • Store and read its own namespaced settings. [live]
  • Preview its own media — images, audio, video — via openMedia (a range URL dropped into <img>/<audio>/<video>) or readFile (bytes → blob:). PDFs can't use the browser viewer inline — see hard limits. [live]
  • Edit a spreadsheet/document in place via readOffice/saveOffice — the file it was opened with, or one in its writable scope (no Office install needed). [live]
  • Download generated files to the user's computer (a blob: + <a download>; the sandbox iframe is granted allow-downloads). [live]
  • (First-party only) call its own server routes — including streaming ones — via moduleRequest / moduleStream, for a module that ships a backend half (durable state, parsing, generation). [live]
  • Queue background jobs and receive progress events. [live] for first-party modules with a registered worker, and for packaged modules whose worker is approved to run in the sandbox (worker.execute); a packaged module with no worker fails a job request cleanly — see hard limits.
  • Declare requirements (runtimes, Python/Node packages, Office apps, services) that are surfaced at review. [enforced] (declaration) / [planned] (provisioning)

Heavier capabilities (granted per module at review)

Off by default and individually granted — admin-approved or admin-HIGH (default-deny with an explicit per-capability grant + quota). See the tier table in capability-api.md:

  • Structured data store — collections of JSON documents scoped to (user, module), for inventory / media-library / task-manager modules (data.store, quota-bound). [live]
  • Background task queue — enqueue a job that runs in the background, then poll or cancel it (jobs.queue, quota-bound in-flight). [live]
  • Scheduled tasks — fire a background job on an interval/cron trigger even while the UI is closed (jobs.schedule, admin-approved; APScheduler, min-interval + max-schedule quota). [live]
  • Heavy compute + native media tools — bigger worker resource tiers and a native-tools image (ffmpeg / imagemagick / libvips / ghostscript) for transcode / render / compress (worker.execute tier + worker.native, admin-HIGH). [live]
  • Broad outbound fetch — a worker may reach any public origin by admin policy, with a per-job byte budget + rate limit (network.fetch.broad, admin-HIGH; still host-mediated, no LAN/SSRF, audited). [live]
  • Persistent background service — run a worker as a long-lived container with restart / crash-loop-breaker / idle-stop supervision (worker.service, admin-HIGH; off by default, host-wide ceiling). [live: lifecycle] — the in-container service protocol + persistent egress channel is the documented next step.
  • Storage introspection — read the user's storage usage / quota (storage.read). [live]
  • Richer client runtime — WebGL / WASM / gamepad / fullscreen / pointer-lock / WebCodecs via per-module client.* flags (client.wasm admin-approved; the rest consent-gated). [live]

Module lifecycle & operational safety (guarantees) #

How the platform handles a module must never put the running system at risk:

  • A module can't crash the platform. A third-party worker runs in an isolated container/subprocess and every failure is caught — a crashing or raising worker fails its job (a clean 500), the API process stays up and responsive. [enforced]
  • No restart needed for anything. Install, uninstall, publish, and version updates are all DB + per-version filesystem changes applied at request time; the catalog is read fresh from the DB on every request, so a newly-approved version is live on the next request — never a process restart. [enforced]
  • A running module only ever touches the current user's files. Every job runs as the signed-in user; inputs are resolved filtered by owner_user_id, outputs land in that user's folders, and a sandboxed worker has no host filesystem access — another user's files are never even mounted. [enforced]
  • Updates auto-flow through the store, queued behind running tasks. A clean version update auto-approves and goes live with no restart. But if one of the module's tasks is currently running, the update is deferred (its package is stashed and the live version is held) and auto-activates the moment the task finishes — so a running task is never disrupted and its package files are never swapped mid-run. Deleting/unpublishing a module while a task runs is refused with a clear "a task is running" message (it would otherwise delete files in use). [enforced] (single API process; a multi-process deployment would add a DB-committed running-marker + stale-job reaper.)

Hard limits (security boundary — will not change) #

These come straight from ../security-model.md and the sandbox model in capability-api.md:

  • Scoped to its own folders — broader access only with consent. By default a module's VFS view is Modules/<Name>/… for the current user. A module may optionally declare files.read.all to read the user's broader workspace (Desktop, Documents, …), but that is elevated: the user must consent (a prompt at install or first use). Even then it can never touch other modules' folders, another user's files, or the real server filesystem — out-of-scope ids are rejected (OUT_OF_SCOPE). [enforced]
  • No undeclared capability. If a permission isn't in the manifest, the host refuses the call (PERMISSION_DENIED) — even if the user is an admin. [enforced]
  • No shell/session access. The UI runs in a sandboxed iframe with an opaque origin: no shell cookies, no localStorage of the shell, no parent DOM, no reading the session token. [enforced]
  • No browser storage in the sandbox. Because the origin is opaque, the module cannot use its own localStorage/sessionStorage/IndexedDB or cookies — access throws. Persist module state through settings.self (HANABI_GET_SETTING/SET_SETTING) or a workspace folder instead. (This is why a panel that reaches for localStorage must be adapted when migrated.) [enforced]
  • No browser PDF/plugin viewer inside the sandbox. The opaque-origin iframe can't render content that relies on the browser's built-in PDF viewer or a plugin — Chrome refuses it ("This page has been blocked by Chrome"). The one flag that would lift this, allow-same-origin, is deliberately off: it would give the module a real origin where the domain-scoped session cookie applies, letting it call the API directly and bypass the capability bridge entirely. To show a PDF, render it yourself (e.g. PDF.js to a <canvas>, or a server-made <img> thumbnail) or offer a download — don't point an <iframe>/<embed> at the PDF. [enforced]
  • No native form submission. The sandbox omits allow-forms, so <button type="submit"> and <form on:submit> never fire — the click is silently swallowed. Use explicit on:click handlers. [enforced]
  • No bundled secrets or native executables. Packages containing .env, secret/credential/password/.pem/.key/id_rsa-like files, or .exe/.bat/.cmd/.ps1/.sh/.dll/.so/.dylib are rejected at validation and re-checked at extraction. [enforced]
  • No path traversal in the package. Members with .. or absolute paths are rejected; extraction is clamped to the module's own directory (zip-slip safe). [enforced]
  • No public publishing. There is no public registry, registration, or password-reset path. Modules are reviewed by an admin; users are seeded. [enforced]

Current limits (being built — see roadmap) #

  • Third-party workers run only in the sandbox, off by default. A third-party module's workerAn optional Python program that does heavy or native work off the browser, in a sealed container. (server-side Python) executes only when it holds the admin-approved worker.execute permission and an operator has enabled a worker-sandbox backend (HANABI_WORKER_SANDBOX_BACKEND). The contract, invocation protocol, approval/quota model, and job wiring are live (Phase 9a), and the worker is a pure bytes → bytes transformation with no db/VFS/session access. The backend default is disabled, so out of the box third-party modules are still UI-only. The real isolation boundary — the container backend (Phase 9b: docker run with no network, dropped caps, no-new-privileges, read-only rootfs, and memory/cpu/pids limits) — is built; turning it on requires Docker on the host (see worker-sandbox-docker-setup.md). The local backend is dev/test only and provides no isolation. [live] (foundation + container backend, default-off) / [gap] (Docker host enablement)
  • Office: in-place editing is a capability; COM automation is server-side. A packaged editor reads and writes spreadsheets/documents through readOffice/saveOffice — the platform's openpyxl/document engine, no Office install required. Rendering through Microsoft Office COM (PowerPoint → PDF, legacy .xls/.doc) runs in a trusted server-side step on a licensed Windows + Office host, never inside the sandbox — and it requires the API process to run on the interactive desktop: a headless restart makes COM fail with E_ACCESSDENIED. See ../operations.md. [enforced]
  • Outbound network is off by default — opt-in, approved, allowlisted. A module can reach only the http(s) origins it declares in dependencies.services, and only after network.fetch is approved at publish; changing the origins re-triggers manual review. UI side: the served CSP connect-src enforces it (everything else is connect-src 'none'); CORS on the target API still applies. Worker side (Phase 9d): the container has no network (--network none); an approved worker's ctx.fetch(url) is performed by the host against the same allowlist (no redirects, private/loopback blocked, size/count caps) — so a worker can't open sockets or reach the LAN. [live]
  • Packaged modules run a server job only via an approved, enabled sandbox. The capability bridge (Phase 2) and the worker registry (Phase 3) are live: a packaged module's UI launches in a sandboxed iframe and files.read/ files.write/settings.self are answered with permission + scope enforcement. HANABI_CREATE_JOB dispatches to a first-party registered worker or, for a third-party module, to the worker sandbox — but only when the module is approved for worker.execute and a sandbox backend is enabled. Otherwise the job request fails cleanly ("this module does not provide a server worker") rather than executing arbitrary server code. [enforced]

Soft limits (quotas & conventions) #

  • Self-contained packages. The opaque sandbox can't load a cross-origin external script (CORS blocks it) and a header-only change won't bust a cached sub-resource, so a module must inline all of its assets — CSS/JS into one HTML document, images as data: URLs. The Svelte bundler (npm run module:build) does this automatically; a hand-authored engine needs a manual inline step. Host-global CSS classes and fonts are not inherited (opaque origin) — define any utility class (e.g. .sr-only) and set font-family in the module itself. [enforced]
  • Storage quota. Files a module writes count against the user's workspace storage limit (storage_limit_mb); writes fail with 413 when exceeded. Admins raise limits in Settings → Accounts. [enforced]
  • File retention. Generated files and jobs carry an expires_at (default 30 days) and are removed by retention cleanup. Treat module outputs as durable-but-not-permanent. [enforced]
  • Upload extension allowlist. Files entering the VFS must match the configured extension allowlist (HANABI_ALLOWED_UPLOAD_EXTENSIONS). [enforced]
  • Window sizing. Min size ≥ 320×240, default ≤ 2200×1400. [enforced]
  • One manifest, one identity. id is a stable slug; versions update under the same id through the draft → version → review pipeline. [enforced]
  • Determinism for review. Packages must ship tests/sample-inputs/ with at least one deterministic sample so a reviewer can exercise the module. [enforced]

If your module needs something not listed here #

That's the design question `architecture.md` §8 answers: a requirement that isn't declared in the manifest doesn't exist. New capability types are added by extending the manifest contract + the capability bridge + the validator together — never ad hoc. File the need against the manifest contract so it can be reviewed and scoped, rather than reaching around the platform.