From Scripts to Sites: Making Agent Deliverables Actually Runnable in Open Nua
Open Nua Engineering
Generating Markdown is easy for an Agent. Turning it into something a user can keep using is much harder: tables in a DOCX must open correctly, charts in a PPTX must be produced by real dependencies, an HTML dashboard must load its own CSS, JavaScript, and images, and the user must be able to inspect it before deployment and withdraw access afterward.
This path can look like “install a few more libraries and add a publish button.” In practice, it crosses two engineering problems that are often treated separately: script dependencies must be reproducible, and delivered code must remain isolated.
Open Nua closed this last-mile gap through two adjacent changes. The first moved Node.js and Python dependencies out of prose in SKILL.md and into standard Plugin projects prepared by aube and uv during installation. The second turned an HTML Output Artifact already produced in a workspace into a previewable, approval-gated, and revocable static-site deployment.
This is not a new build farm or general hosting platform inside Desktop. The boundary is intentionally narrow: a Plugin runs scripts reproducibly; an Artifact declares the result; Desktop owns preview and user approval; Backend owns immutable deployment facts; Gateway performs governed delivery; and the browser runs the site inside an opaque sandbox.
A successful report exposed an unreliable runtime
The work began with a real document task. The report was eventually delivered, but the generation path encountered Python ABI and module-resolution problems, temporary dependency preparation, NODE_PATH changes, and several fallback attempts.
The dangerous part is that the final state looked successful. If we only inspected the output file, the intermediate experimentation could be mistaken for normal Agent reasoning. From a product perspective, however, every temporary installation and environment guess makes the result dependent on one machine's current state—and makes the next run free to fail differently.
At the time, the DOCX, PDF, PPTX, and XLSX Skills were separate resource directories. Their instructions asked the model to install global npm packages, create temporary Python environments, or locate LibreOffice and Poppler. The issue was not insufficient prose. One natural-language document was trying to serve as operational knowledge, dependency declaration, and runtime repair procedure at the same time.
We froze a simpler rule:
A Skill teaches an Agent how to do the work. If that work needs third-party script dependencies, those dependencies belong to a Plugin—not to the prompt.
Instruction-only Skills remain independent. A Skill must move into a Plugin package only when it carries Node.js or Python scripts that require third-party dependencies. Even a one-Skill package uses a skill-only Plugin instead of creating a parallel “executable standalone Skill” format.
Use the language ecosystem's facts instead of inventing another dependency format
An Agent platform can easily drift toward a custom abstraction: define a cross-language dependency manifest, then add entrypoints, cache keys, environment states, and installation APIs. The result looks unified but duplicates problems already owned by npm, uv, and their ecosystems.
Open Nua chose the opposite direction. A dual-runtime Plugin has a conventional root:
builtin-document/
├── plugin.json
├── package.json
├── package-lock.json
├── pyproject.toml
├── uv.lock
└── skills/
├── docx/
├── pdf/
├── pptx/
└── xlsx/
JavaScript and TypeScript dependencies, Node constraints, and named scripts come from package.json plus one canonical lockfile. Python dependencies and named commands come from pyproject.toml and uv.lock. plugin.json continues to carry Plugin identity and contributions without duplicating dependencies or entrypoints.
The Node side uses aube for existing lockfiles, isolated node_modules, the content-addressable store, and runtime selection. The Python side uses uv for locking, .venv, cache, and command execution. Open Nua defines only how those standard projects enter an archive, when they are synchronized, which minimum Host security policies apply, and what an Agent turn may execute.
That removes three facts that would otherwise drift apart: Skill prose no longer describes installation, the Host manifest does not repeat dependencies, and the platform does not maintain another resolver.
Move dependency preparation out of the Agent turn
A declarative project alone does not make execution governed. The key boundary is separating environment preparation from business-script execution.
| Lifecycle | What may happen | What must not happen |
|---|---|---|
| pack / publish | Validate standard projects and locks; run frozen conformance | Publish missing or multiple locks, path/git dependencies, or weakened Host policy |
| install / update / repair / local reload | Host invokes pinned aube/uv; switch the active version atomically after preparation succeeds | Break the still-runnable previous version when preparation fails |
| Agent run | Invoke only declared scripts and commands | Network installation, lock updates, temporary environments, or NODE_PATH / PYTHONPATH guessing |
Execution converges on two forms:
aube run --no-install <script> -- <args>
uv run --locked --no-sync <command> <args>
If an environment is damaged, the Agent receives a clear “repair or reload Plugin dependencies” result. It does not receive a blank cheque to keep trying npm install -g, pip install, or uv add. Network access, registry credentials, proxies, disk exhaustion, and native-build failures belong to the Host installation lifecycle, not to a document task.
Desktop pins and bundles aube and uv and reuses its bundled Python 3.12. Node.js itself is not bundled. The Host first validates an allowlisted PATH candidate against the Plugin's semver range, LTS status, platform, and architecture, with Node.js 22 as the minimum. If no compliant runtime exists, aube may download and cache one only during installation, update, or repair. An Agent run never triggers that download.
Validate the dual runtime with a real document Plugin, not only a schema fixture
The easiest test for a package format is a fake hello-world Plugin. It proves that fields can be parsed but says nothing about native addons, two locks, packaged candidates, and real document work operating together.
We therefore consolidated the four existing document Skills into a builtin document Plugin and made it the first real adopter:
- rich DOCX and PPTX generation uses locked Node dependencies;
- image and chart paths load the native
sharpaddon; - editing, inspection, and validation helpers use Python
[project.scripts]; - LibreOffice and Poppler remain separate system-capability preflights rather than pretending to be uv dependencies;
- the distribution archive carries no
node_modules,.venv, cache, or Python bytecode.
In an isolated macOS arm64 packaged profile, Desktop discovered all four Skills, performed frozen aube/uv synchronization, generated DOCX and PPTX through Node, inspected them through Python, loaded native sharp, repaired the environments, and removed the Plugin. Once prepared, real work ran under the --no-install and --no-sync constraints.
That evidence matters more than an “installation succeeded” message. It demonstrates that the Plugin archive stays portable, the installed copy may own local environments, and an Agent turn does not have to become a package manager again.
Once scripts are runnable, the result is still only a workspace file
Dependency management closes only the first half of the path. An Agent can now produce DOCX, PPTX, or HTML reliably, but HTML often derives its value from interaction: filtering a dashboard, changing a chart, expanding evidence, or playing a local animation. Flattening it into a screenshot removes that interaction, while opening it directly through file:// exposes an unnecessary local boundary.
Open Nua therefore defines a static site as a delivery mode for an Output Artifact, not as another Plugin runtime. There are only two accepted inputs:
summary.html
report-site/
├── index.html
└── assets/
├── site.css
├── site.js
└── chart.svg
Any standalone HTML file can be a single page. Only an index.html treats its directory as a multi-asset site root. Neither the Skill nor the user creates a ZIP. Desktop Main re-resolves the workspace path from the trusted Artifact and reads and packages the source only after the user confirms deployment.
Ownership is important here. The static-site Skill defines the delivery shape without owning content generation. The Output Artifact connects a generated result to preview and deployment. Desktop does not duplicate a site generator, and Backend does not run build commands.
Preview is not a miniature deployment
Users should inspect a result before deciding whether it may create a remote side effect. Internal preview and deployment therefore follow separate paths.
Preview stays inside the local process. Desktop Main issues a random openneo-preview:// URL for each preview, and Renderer never receives an absolute host path. The protocol handler rechecks realpath containment, resource type, and size for every CSS, JavaScript, image, font, and media file. Symlink escape and path traversal are rejected before reading.
The iframe uses sandbox="allow-scripts". Bundle JavaScript can run, but there is no allow-same-origin; fetch, WebSocket, workers, frames, and forms cannot reach external networks or Desktop's top-level capabilities. Preview creates no Backend deployment and uploads nothing to RustFS.
Only after the user selects deploy and confirms again does Desktop package and upload the site. Agents, Plugins, Renderer, automations, and historical messages cannot substitute for this Main-owned confirmation.
Deployment is not “put the ZIP in object storage”
A successful ZIP upload does not make a safe website. During finalize, Backend performs authoritative validation again. It rejects traversal, absolute paths, backslashes, duplicate paths, symlinks, encrypted entries, unknown compression methods, compression bombs, excessive file counts, and extensions outside the allowlist.
After validation, each resource is written to an immutable deployment prefix in a private RustFS bucket. The manifest records path, content type, size, SHA-256, and object locator; the transport ZIP is then deleted.
Gateway does not receive arbitrary bucket or object-download authority. The user-facing surface exposes only:
GET /site/{token}/
GET /site/{token}/{asset_path}
For each request, Backend re-resolves the deployment, token, visibility, expiry, and in-site path, then grants a 60-second read authorization. Gateway verifies size and SHA-256 again before returning the body. If the digest differs, no body is disclosed.
The final HTML response uses CSP sandbox allow-scripts without allow-same-origin. CSS, JavaScript, images, and fonts continue to work, but site code cannot read Gateway cookies, localStorage, or same-origin APIs. connect-src 'none', service-worker rejection, no-store, noindex, and a Redis rate limit further narrow the delivery surface. If Redis is unavailable, delivery fails closed.
Sharing is a side effect that remains governed
The first version supports exactly two visibility modes: owner_only and public, with the former as the default.
Every owner_only request requires a Gateway session whose actor matches the deployment owner. A public deployment uses an unguessable bearer token with at least 256 bits of entropy. The database stores only its HMAC hash—not the raw token or a presigned URL.
Changing visibility is more than toggling a Boolean. It increments the version and rotates the token, invalidating the old URL immediately. Revoke and expiry also stop subsequent resolution immediately. Expanding to public, narrowing back to owner-only, and revoking a deployment each require confirmation for the current user action. The share UI keeps the raw token only in component state and never writes it back to Artifact metadata or conversation history.
Static-site deployment therefore follows the same rules as other Agent side effects: private by default, explicitly approved, minimally capable, revocable, and unwilling to mislabel an uncertain intermediate state as success.
What this path still is not
Open Nua currently delivers governed preview and deployment for already-built static assets. It is not a general application-hosting platform.
It provides no server-side build command, SSR, Functions, WebSocket backend, database, CMS, custom domain, or arbitrary outbound network. Site JavaScript can drive local interactions, charts, filters, and animation, but it cannot call Gateway APIs directly. If future sites need data, they will require a separate capability proxy—not a same-origin exception punched into the opaque sandbox.
Likewise, aube and uv provide reproducible installation and execution for Node and Python projects inside Plugins. They do not promise automatic installation of arbitrary system binaries, do not let Python sdists execute arbitrary build backends on user machines, and do not force instruction-only Skills into Plugin packages.
Stating these limits prevents two delivered local capabilities from being misrepresented as a Managed Runtime or cloud application platform that does not yet exist.
Six lessons from closing the last mile
1. A generated file is not a delivered result. Whether users can inspect, understand, share, and revoke it determines whether an Artifact actually enters the product lifecycle.
2. Dependencies are package facts, not prompt suggestions. Once a script needs a third-party library, a Plugin's standard project and lockfile should own it.
3. Installation and execution need separate lifecycles. Frozen synchronization may reach governed dependency sources; an Agent run may only execute prepared entrypoints.
4. Preview and deployment are different side effects. Local in-process preview must not silently create remote objects, and deployment must follow explicit approval after inspection.
5. Running JavaScript does not require same-origin authority. An opaque sandbox preserves interaction while severing cookies, storage, APIs, and external networking.
6. The best platform abstraction is often the one not invented. Reuse package.json, lockfiles, pyproject.toml, uv, Artifacts, object storage, and HTTP instead of recreating a resolver, runtime manifest, build farm, and publishing protocol.
No single omnipotent component connects a locked script to an interactive report in a browser. Each layer owns only its own facts: the Plugin owns code and dependencies; the Host owns installation and approval; the Artifact owns the result reference; Backend owns the deployment; Gateway owns delivery policy; and the browser sandbox owns the last isolation boundary.
The last mile for an Agent is not generating more things. It is making generated results runnable, inspectable, shareable, and retractable.