Limits & quotas
What modules can and cannot do, and the quotas on files, workers, jobs, and data.
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. Seeexamples/modules/broad-reader. - Read files the user explicitly picks from its own
inputfolders. [live] - Write generated files into its own
outputfolder. [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-gatedfiles.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>) orreadFile(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 grantedallow-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.executetier +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.wasmadmin-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 declarefiles.read.allto 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
localStorageof 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/IndexedDBor cookies — access throws. Persist module state throughsettings.self(HANABI_GET_SETTING/SET_SETTING) or aworkspacefolder instead. (This is why a panel that reaches forlocalStoragemust 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 expliciton:clickhandlers. [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/.dylibare 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.executepermission 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 purebytes → bytestransformation with no db/VFS/session access. The backend default isdisabled, so out of the box third-party modules are still UI-only. The real isolation boundary — the container backend (Phase 9b:docker runwith 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 (seeworker-sandbox-docker-setup.md). Thelocalbackend 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'sopenpyxl/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 withE_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 afternetwork.fetchis approved at publish; changing the origins re-triggers manual review. UI side: the served CSPconnect-srcenforces it (everything else isconnect-src 'none'); CORS on the target API still applies. Worker side (Phase 9d): the container has no network (--network none); an approved worker'sctx.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.selfare answered with permission + scope enforcement.HANABI_CREATE_JOBdispatches to a first-party registered worker or, for a third-party module, to the worker sandbox — but only when the module is approved forworker.executeand 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 setfont-familyin the module itself. [enforced] - Storage quota. Files a module writes count against the user's workspace storage limit (
storage_limit_mb); writes fail with413when 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.
idis 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.