Notebooks
A notebook is the one artifact that runs. A reader opens the URL and the code executes in their browser under Pyodide — no server, no session, no kernel of yours doing work for a stranger. That is what makes it publishable in the same breath as a static report.
factory-publish notebook analysis.py --out ./bundle --vendor-runtime 0.28.0
factory-publish publish notes-2026 ./bundle --title "Where the effect went"
The second command is an ordinary publish, so everything in Artifacts applies: versions, an alias, visibility, sharing, takedown.
The export does not run your notebook
This is worth stating plainly because the tooling itself claimed otherwise for a long time, and the wrong claim was load-bearing — it was the stated reason notebooks were kept off unattended paths.
marimo export html-wasm parses the notebook. It does not execute the cells,
and it does not even import the module: a statement at file top level does not
run either. Measured rather than reasoned about — a notebook whose cell writes a
file exports without writing it, and a notebook that imports a package which does
not exist and then raises SystemExit exports cleanly.
Two things follow:
- Rendering somebody's notebook is not running it. The dangerous-looking step is not the dangerous step.
- The timeout is still there, and still worth having, because any subprocess can hang. It just is not protecting you from your own cells.
What does reach the network at render time is --vendor-runtime, which fetches
Pyodide. That, not execution, is the one thing this template does that a confined
renderer would have to be allowed.
--vendor-runtime is not optional in practice
Without it the bundle keeps marimo's CDN loader, and it will not boot on an
origin that enforces the policy — which this one does. The command will let you
build one anyway with --allow-unvendored-runtime, but that is a diagnostic;
the result must never be published, and the render says so.
Vendoring downloads Pyodide at the version you name, from a pinned allowlist of hosts, and records a digest for every file it embeds. The bundle is then self-contained: it boots with no third-party request at all, which is what lets it be served under a policy that forbids them.
runtime pyodide 0.28.0 — 11 files, 4 package(s), 12.4 MB
pinned runtime/pyodide.asm.wasm sha256:…
Expect the bundle to be large — a vendored runtime is tens of megabytes. That is the price of a page that boots from one origin and keeps working when a CDN does not.
Why it is compute class, and what that buys
Bundles are served under one of three policies. A notebook is compute, which is
the only one that grants wasm-unsafe-eval — Pyodide cannot run without it.
Two details in that policy matter to anyone debugging a notebook that works locally and not when published:
wasm-unsafe-eval, neverunsafe-eval. The narrow directive permits WebAssembly compilation and nothing else. Python-in-the-browser works;eval()of arbitrary JavaScript still does not.- COOP and COEP are on, which is what unlocks
SharedArrayBuffer. It also means any resource the page pulls must be same-origin or explicitly cross-origin-isolated — another reason the runtime is vendored rather than fetched.
What framing costs, exactly
Cross-origin isolation has to be granted by the top-level document and every
frame above yours. The viewer page
is not isolated, so a notebook opened there has no SharedArrayBuffer — while
the same notebook opened at its bare URL does.
What that costs is one feature, and it is worth knowing which. marimo
feature-detects: with isolation it allocates an interrupt buffer, and without it
it logs Not running in a secure context; interrupts are not available. and
carries on. So a framed notebook boots, runs its cells, and renders exactly as it
otherwise would — you just cannot interrupt a running cell.
Send someone the page. If interrupting long-running cells is the point of a particular notebook, send the bare URL for that one.
The class is read from what the renderer recorded, never assumed. A compute
bundle published as static would be served from the wrong origin under a policy
it cannot boot under, so publish reads the record rather than taking your word
for it.
Checking it before anyone else sees it
factory-publish serve ./bundle --class compute
Loopback only, with the real headers for its class. That last part is the
point: a bundle checked without its Content-Security-Policy is a bundle checked
against something the world never sees. There is a --no-csp escape for finding
out what a bundle needs, but a bundle that works that way has been told
nothing.
Agents
notebook_render is on the MCP surface,
so an agent can render one; publishing it goes through the outbox like every
other publish. It takes the same vendor_runtime, and the tool's answer says
explicitly when no runtime was embedded — because the failure mode is a bundle
that publishes fine and then does not boot, which is discovered by a reader
rather than by the run.
Where to go next
- Artifacts — versions, visibility, sharing, takedown
- The component gallery — what the rendered surfaces look like