Wardian docs

Wardian package format

Format version: 2 (format 1 packages run unchanged; see 2.5) Applies to: Wardian 0.4 and later (format 1: Wardian 0.3 and later)

This document says what a Wardian package is, what a host guarantees to it, and what a package must not do. The words MUST, MUST NOT, SHOULD and MAY are used as in RFC 2119.

wardian check <package> tests a package against this document. The JSON Schemas in schemas/ describe app.json and suite.json for editors.


1. Terms#

TermMeaning
packageOne folder that a host runs as one entry in its app list.
module appA package whose main part is a WebAssembly module, app.wasm.
pageAn HTML page inside a package that the host shows as the app's interface.
suiteA package that holds several suite apps described by a suite.json.
suite appOne sealed part of a suite: an app.js, and optionally a view.html.
hostThe Wardian server and its main page.
kernelThe host page that runs a suite and passes messages between its apps.
frameThe sandboxed browser frame one page or one suite app runs in.

2. Versions#

2.1. app.json and suite.json MAY have a "format" field, a whole number from 1. A file without it is format 1.

2.2. A host MUST NOT run a package whose format is newer than the host supports. It MUST say why in the app list instead.

2.3. Within one format version, changes are additive only. A host MUST ignore fields it does not know. wardian check reports them as warnings, because they are often typos. One exception: an unknown key inside channels is an error, because a misspelt send or receive would silently leave the package with no channel.

2.4. A package SHOULD state "format": 1 once it depends on anything in this document.

2.5. Format 2 adds one thing: channels between packages (channels, section 6.9). A package that declares channels MUST state "format": 2. Then a format-1 host refuses it with "Update Wardian" (2.2) instead of failing when the app runs. Everything else is unchanged, so a format-1 package runs on a format-2 host as before.

3. Package layout#

3.1. A package is one folder. The folder's name is the package's name.

3.2. A package MUST contain app.wasm or suite.json at its top. If it contains suite.json, it is a suite, and the host ignores app.wasm and any page.

3.3. Names. Every folder and file name in a package MUST match [A-Za-z0-9_-][A-Za-z0-9_.-]*: letters, digits, -, _ and ., not starting with .. The host does not serve other names.

3.4. Never served: hidden files, and anything inside a folder named node_modules or target.

3.5. Limits:

LimitValue
Folder depth below the package top8
Files in one package2,000
One file64 MB
All files, unpacked256 MB
A zip, uploaded or fetched by link100 MB
A zip, fetched from Google Drive64 MB
Entries in a zip10,000

4. app.json#

app.json is optional, at the top of the package.

{
  "format": 1,
  "title": "USL analyzer",
  "description": "Fits the Universal Scalability Law to load-test results.",
  "page": "demo/index.html"
}
FieldTypeMeaning
formatintegerSee section 2.
titlestringThe name shown in the app list. Default: the folder name.
descriptionstringShown under the title.
pagestringPath of the app's page in the package. See 5.3.
channelsobjectFormat 2. Channels the page may use to talk to other packages: { "send": [...], "receive": [...] }. See 6.9.

5. Module apps#

5.1. app.wasm MUST be a WebAssembly binary, version 1 (it starts with \0asm and 01 00 00 00).

5.2. Without a page, the host shows each exported function with one input per parameter. It calls the function with JavaScript numbers. If the call fails because a parameter is i64, it calls it again with BigInts. It shows other exports (memory, globals) by name.

The host provides one import, env.log(value), which writes to the host's output log. The host replaces every other function import with a stub that logs the call and returns 0, so the module still loads. A module that imports a memory, table or global does not load without a page.

5.3. With a page, the host shows the page instead, in a frame. The page is page from app.json, or else the first of these that exists: index.html, demo/index.html, www/index.html, web/index.html.

5.4. A page loads its own files with relative URLs (../pkg/app.js, ./app.wasm). The host serves the package's folder tree as it is, at /apps/<name>/<path>. A URL that ends in / serves index.html from that folder.

5.5. Content types. The host serves:

ExtensionContent type
.wasmapplication/wasm
.js, .mjstext/javascript
.htmltext/html; charset=utf-8
.csstext/css
.jsonapplication/json
.svgimage/svg+xml
.png, .jpg, .jpeg, .gif, .webp, .icothe matching image type
.woff2font/woff2
.txt, .md, .ts, .rs, .toml, .shtext/plain; charset=utf-8
anything elseapplication/octet-stream

Every file is sent with X-Content-Type-Options: nosniff and Access-Control-Allow-Origin: *.

5.6. Pages run sandboxed. Every HTML and SVG file of a package is sent with a Content-Security-Policy that starts with sandbox allow-scripts allow-forms allow-modals allow-popups allow-downloads and allows requests only to the package's own folder (/apps/<name>/), the host's page library (/sdk/), Google Fonts, and data: and blob: URLs (ADR-2610081003). The browser gives the page a unique, throwaway origin. So a page:

6. Suites#

A suite is several small apps that share one screen. Each suite app runs in its own frame, and the apps talk only through the kernel.

One job per part (ADR-2610080900): inputs, each view of the result and each export SHOULD be parts of their own, inputs in aside and results in main, so viewers can arrange them (6.11). Code that several parts need goes in a shared file listed in scripts, not copied. wardian check warns about a part whose app.js is over 400 lines, and about a suite whose only panel has more than one <h2>.

6.1. Layout#

my-suite/
  suite.json
  apps/<name>/app.js       each suite app's code (required)
  apps/<name>/view.html    its markup (if it has a slot)
  ...                      shared files named in suite.json

A host MAY let each viewer rearrange a suite's panels for themselves: change their order, move them between the two columns, hide them, or use one column. Wardian calls this Arrange and keeps the layout for the viewer, not in the package. A hidden panel's app still runs. So an app MUST NOT depend on where its panel sits, or on being visible.

6.2. suite.json#

{
  "format": 1,
  "title": "USL scalability lab",
  "styles": ["https://fonts.googleapis.com/css2?family=...", "core/style.css"],
  "scripts": ["core/lib.js"],
  "header": "core/header.html",
  "columns": "minmax(280px, 340px) minmax(0, 1fr)",
  "apps": [
    { "name": "engine", "scripts": ["core/engine.js"],
      "emits": { "engine:status": { "retain": true } },
      "provides": ["analyze", "curve"], "caps": ["worker", "source"] },
    { "name": "inputs", "slot": "aside", "wrap": "<aside class=\"inputs\">",
      "emits": { "data:changed": { "retain": true } }, "caps": ["storage"] },
    { "name": "chart", "slot": "main", "wrap": "<section id=\"chartSec\">",
      "listens": ["analysis:ready"], "needs": ["engine.curve"] }
  ]
}
FieldTypeMeaning
format, title, descriptionAs in app.json.
styleslistStylesheets for every frame, in order. Each is a package path (inlined) or a URL starting https://fonts.googleapis.com/. No other outside URL is allowed.
scriptslist of pathsScripts inlined into every suite app's frame, before the app's code.
headerpathHTML shown across the top, in its own frame.
columnsstringCSS grid-template-columns for the two columns. Default minmax(280px, 340px) minmax(0, 1fr).
appslistThe suite apps, below. At least one.

Each entry in apps:

FieldTypeMeaning
namenameRequired. Unique in the suite.
slot"aside" or "main"aside: the left column, sticky, scrolls alone. main: the right column, in list order. Absent: the app has no view and runs out of sight.
wrapstringOne opening tag that the view sits in, e.g. <section id="chartSec">. The host adds data-app="<name>". Default <div>.
dirpathFolder with app.js and view.html. Default apps/<name>.
scriptslist of pathsExtra scripts for this app only, inlined after the suite's scripts.
emitsobjectTopics the app may emit: { "topic": {} }, or { "topic": { "retain": true } }.
listenslistTopics the app may listen to.
provideslistMethods other apps may call on this app.
needslistMethods this app may call, as "app.method".
capslistCapabilities, from 6.6.
channelsobjectFormat 2. Channels to other packages: { "send": [...], "receive": [...] }. See 6.9.

emits, listens, provides, needs, caps and channels together are the app's contract.

6.3. The frame#

The host builds each suite app's frame as one document, in this order:

  1. every entry of styles: a <link> for a font URL, else the file inlined in <style>;

  2. the view: view.html inside wrap. In the main slot, the host wraps it in <main>, and for every app after the first, it adds a hidden element before it, so CSS rules like main section:first-child behave as they would on one page;

  3. every entry of scripts, then the app's scripts, each inlined as <script id="<file stem>-src"> (so core/lib.js becomes lib-src);

  4. the kernel shim, which defines Kernel;

  5. app.js, then Kernel.start().

    Inlined scripts MUST NOT contain </script, and inlined styles MUST NOT contain </style.

The frame runs under this policy, so the browser blocks every network request it could make:

sandbox allow-scripts allow-forms allow-modals allow-popups allow-downloads;
default-src 'none'; script-src 'unsafe-inline' 'wasm-unsafe-eval' blob:; worker-src blob:;
style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com;
img-src data: blob:; connect-src 'none'; form-action 'none'; base-uri 'none'

A suite app gets everything it needs from the kernel or from its own frame. It MAY compile WebAssembly ('wasm-unsafe-eval' allows that, not JavaScript eval), and it loads the bytes with ctx.asset. Images MUST be inlined as data: URLs or made as blob: URLs.

6.4. app.js#

app.js MUST call Kernel.register exactly once:

Kernel.register({
  name: 'chart',
  listens: ['analysis:ready'],
  needs: ['engine.curve'],
  init(ctx) { /* ... */ }
});

The contract given to register MUST equal the contract in suite.json. Order of list items does not matter. If they differ, the kernel reports a fault and does not start the app. The kernel enforces the contract in suite.json, never the one in app.js. The copy in app.js exists so the same code also runs in a single-page build.

register MAY also take snapshot(ctx), for Save as web page (7.4). It returns HTML text or an element, or a promise of either, and the saved page shows that inside the app's root (the element made from wrap) in place of what the frame shows. An element is copied the same way as the frame (below). Use it to put more in the file than fits on screen, such as every row of a table, and say so in the text when you cut. Without it, or when it returns null, throws or rejects, the host copies what the frame shows: field values written in, the chosen option marked selected, ticked boxes checked, each canvas and inline SVG as a data: image, and blob: images read into data: URLs. Hidden panels are left out. A part that does not answer within 10 seconds is shown as "this part could not be saved". snapshot is not part of the contract.

Kernel.register({
  name: 'rows',
  listens: ['table:ready'],
  init(ctx) { /* ... */ },
  snapshot(ctx) { return '<table>…every row…</table><p>All 2,410 rows.</p>'; }
});

6.5. ctx#

init(ctx) receives a frozen object with these members:

MemberMeaning
nameThe app's name.
rootThe element made from wrap.
$(sel), $$(sel)querySelector and querySelectorAll inside root ($$ returns an array).
el(tag), text(s)Create an element or a text node.
emit(topic, payload)Send payload to every app that listens to topic. Throws if topic is not in emits.
on(topic, fn)Call fn(payload) for each message on topic. Throws if topic is not in listens. If the topic is retained, fn first gets the latest payload.
provide({ method: fn })Make methods callable by other apps. fn(args) may return a value or a promise. Throws if a method is not in provides.
call(app, method, args)A promise of the other app's result. Rejects if "app.method" is not in needs, if the other app does not provide it, if it does not start within 15 seconds, or if the method throws.
store.get(key), store.set(key, value)Needs storage. get is synchronous and returns null for a missing key. Values MUST be JSON-compatible.
asset(path)Needs asset. A promise of an ArrayBuffer with the bytes of path, a file in this suite's package, e.g. ctx.asset('text.wasm').
cap(name)Needs claude:<name>. A promise of the capability, or null if this host cannot provide it.
channel(name)Format 2. { send(data), on(fn) } for a channel to other packages. See 6.9.
observe(el, fn)Call fn when el changes size.
source(id)Needs source. The text of an inlined script, e.g. ctx.source('lib-src').
spawn(code)Needs worker. A Web Worker that runs code, or null if workers are not available.

Every payload, argument and result is copied (structured clone). No object is ever shared between apps. A value that cannot be copied, such as a function, makes emit or call throw.

6.6. Capabilities#

CapabilityGrantsProvided by
storagectx.store. The host keeps the data per suite and per app, in its data folder (ADR-2610071055), with a copy in the browser for a viewer the host does not let write.kernel
assetctx.asset: read files of this suite's own package, such as .wasm modules or data.kernel
workerctx.spawnthe frame
sourcectx.sourcethe frame
claude:downloadsctx.cap('downloads') → { save({ filename, data }) }. filename matches [A-Za-z0-9_. -]{1,120}. data is a string, Blob, ArrayBuffer or typed array. Resolves to { status: 'saved' }.kernel
claude:samplectx.cap('sample') → a function sample(prompt, {signal, onText, modelTier}) resolving to { text, truncated }, and sample.json(prompt, opts) resolving to parsed JSON. Errors carry e.code (not_granted, rate_limited, refused, invalid_json, prompt_too_large, over_budget, cancelled, error); over_budget means the package used its daily token cap, set by an admin, and lasts until the next UTC day. Wardian answers through the Claude provider set up in Settings, the Anthropic API or Amazon Bedrock, and resolves to null when there is none, so apps MUST handle null. The user allows each package once, as for splunk.host server
splunkctx.cap('splunk') → { status(), search({ search, earliest?, latest? }), jobs(), wait(id), cancel(id) }. status() resolves to { ready }. search resolves to { fields, rows, truncated, messages, job }, at most 10 000 rows. The server runs the search with its own Splunk account; the app never sees it. jobs() resolves to this package's background jobs of the last hour, newest first: { id, app, kind, label, state, progress, started, ended, elapsed, error }, kind one of splunk.search, splunk.into, ai.sample, state one of running, done, failed, cancelled, progress the rows loaded so far or null. wait(id) resolves to a job's result when it is done, as the call that started it would have; cancel(id) stops it (6.6.1).host server
dbctx.cap('db') → the package's own SQLite database, kept by the host (ADR-2610071219): query({ sql, params }) → { columns, rows, changed, truncated } for one statement, at most 1 000 rows; page({ table, offset, limit, orderBy, desc, where, params }) → { columns, rows, total, offset }, limit at most 1 000, where a condition with ? (or ?N) placeholders, read-only; insertRows({ table, columns, rows, create, replace }); tables(); readPage({ package, ...page }) reads another package's table, read-only, after the user allows it once (asked as tables.<package>); searchInto({ search, earliest, latest, table }) loads a Splunk search into a table, up to 1 000 000 rows (needs splunk too). The host refuses ATTACH, DETACH, loading extensions and pragmas that set anything; a database may hold 1 GB and a statement may run 10 seconds.host server

6.6.1. Long calls run as background jobs

A Splunk search, a load into a table (searchInto) and a claude:sample request can take minutes (ADR-2610072118). The host MUST NOT hold one HTTP request open for them: the kernel asks the server to start a job, which answers with its id at once, then asks how the job is going with short requests (the first after 250 ms, then every second for the first 10 seconds, then every two) and settles the app's promise with the result. Apps see the same promises as before; the result also carries the job's id as job. A job keeps running when its app is closed, so an app MAY look for its own job with jobs() when it opens and pick it up with wait(id). A job is shown only to the package that started it (and to the host's own pages); the server runs the same permission checks before it starts one. Finished jobs are kept for an hour, at most 50 per package, and a restart of the host forgets them. Cancelling a Splunk job also cancels the search on the Splunk server; rows already loaded into a table stay.

A splunk search MUST NOT run until the user allows the package, the same way as a channel (6.9), with the answer kept under mode use. The server MUST check that answer, and that the calling app lists splunk in suite.json, on every search. A host without Splunk resolves ctx.cap('splunk') to null, so apps MUST handle null.

A host MUST NOT grant a capability that the app's contract in suite.json does not list.

6.7. Kernel guarantees#

  1. A suite app can reach nothing outside its frame except through the kernel.
  2. The kernel delivers a message only to apps whose listens contain its topic.
  3. The kernel replays the latest payload of a retained topic to every app that starts listening.
  4. The kernel forwards a call only if the caller needs it and the callee provides it. Only the called app can answer it.
  5. A call to an app that has not finished starting waits for up to 15 seconds.
  6. Any refusal is recorded as a fault. Faults show on the suite page and in Kernel.faults(). Kernel.trace() returns the last 400 events. Kernel.apps() returns every contract.

6.8. Frame–kernel messages (informative)#

The shim and the kernel exchange these postMessage objects. Apps do not use them directly. They are listed for anyone who writes another host or shim. The kernel identifies the sender by its frame, never by the message's content.

From frameMeaning
{k:'hello', app}The frame is ready.
{k:'emit', topic, payload}ctx.emit
{k:'on', topic}ctx.on
{k:'provide', methods}ctx.provide
{k:'call', id, app, method, args}ctx.call
{k:'result', id, ok, value | error}The answer to an invoke.
{k:'store', key, value}ctx.store.set
{k:'asset', id, path}ctx.asset
{k:'cap', id, name}, {k:'capop', id, name, op, args}ctx.cap and a capability's methods.
{k:'size', h, bg}The frame's content height and background color.
{k:'chsend', id, channel, data}, {k:'chon', id, channel}ctx.channel(name).send and .on.
{k:'fault', message}An error inside the app.
{k:'snapshot', id, html, css | error}The answer to snapshot: the frame's rendering (7.4).
From kernelMeaning
{k:'boot', contract, store}The contract from suite.json, and the stored data.
{k:'msg', topic, payload}A message for ctx.on.
{k:'invoke', id, method, args}Another app calls a provided method.
{k:'reply', id, ok, value | error}The answer to call, asset, cap, capop, chsend or chon.
{k:'chmsg', channel, data, from, at}A message on a channel, for ctx.channel(name).on.
{k:'snapshot', id, css}Save as web page asks for a rendering; css asks for the frame's styles too.

6.9. Channels between packages#

Topics (6.2) connect the apps of one suite. Channels connect separate packages: a page app and a suite, or two suites, each installed on its own. Because that crosses the line between packages, the user decides, the way a phone asks before an app uses the camera.

  1. Declare. A package lists its channels: in app.json for a page app, in each suite.json entry for a suite app. send lists the channels it may send on; receive the channels it may read. A channel name is 1–64 characters: lowercase letters, digits, ., -, _, starting with a letter or digit. The package MUST state "format": 2.
  2. Ask. The first time a package sends or receives on a channel, the host asks the user: "loan-planner wants to send messages on the channel budget." The answer, allow or don't allow, is kept per package, channel and direction. The host MUST NOT ask about, or allow, a channel the package does not declare. Closing the question without an answer refuses this use only.
  3. Revoke. The host lists every answer, and the user can take one back. The package is then asked again.
  4. Deliver. The host sends each message to every package that is allowed to receive on that channel, in every Wardian tab of this browser, except the sender. It stamps each message with the sending package's name, so a package cannot pretend to be another.
  5. Keep the latest. The host keeps the latest message on each channel. A package that starts receiving gets it first, like a retained topic.

A table too large for one message travels as a dataset reference: the message carries { dataset: { package, table, total, columns, fields } } (and MAY carry the first rows inline), and a receiving suite app that declares db reads the rows with ctx.cap('db').readPage({ package, table, … }), read-only, after the user allows it to read that package's tables.

Data MUST be JSON-compatible and at most 256 KB, counted as the characters of its JSON text. A package may send at most 100 allowed messages in 10 seconds from one browser tab. The permission belongs to the package, not to one app inside a suite: in a suite, only the entries that declare a channel can use it.

In a suite app:

Kernel.register({
  name: 'view',
  channels: { receive: ['budget'] },      // the same as in suite.json
  init(ctx) {
    ctx.channel('budget').on((data, { from }) => { /* from: the sending package */ })
      .catch(e => { /* the user did not allow it */ });
  }
});

In a page app, load the host's small library, then use the same calls:

<script src="/sdk/wardian.js"></script>
<script>
  wardian.channel('budget').send({ monthly: 1798.65 })
    .catch(e => { /* not allowed, or the page was opened outside Wardian */ });
</script>

send(data) resolves once the message is delivered. on(fn) resolves once receiving is allowed. Both reject when the user does not allow the channel. A page opened on its own, outside Wardian's app list, has no channels: both calls reject.

6.10. Standard components#

A host provides a small set of standard elements, so apps look and behave alike without bringing their own copies. A host MUST define them in every suite frame and in /sdk/wardian.js for page apps. They need no capability. An app MUST still work, in plain form, where an element is not defined.

ElementDoes
<wardian-progress>A progress bar. Without value it shows work of unknown length; with value and max it fills. Attributes label, detail, elapsed (a running clock), cancelable (a Stop button that fires cancel). Methods start(label, opts), update({value, max, label, detail}), done(label), fail(label); property seconds. It has the progressbar role and stops moving for reduced motion. Colours come from --wardian-progress-color, --wardian-progress-track and --wardian-progress-error.

The component library is different: it is copied in, not provided. wardian add COMPONENT... PACKAGE copies each component's files (plain CSS, and a small script for tabs, dialog, toast and tooltip) into the package's ui/ folder, with ui/theme.css, the tokens they all read (--w-bg, --w-fg, --w-primary, --w-radius …). For a suite it adds them to suite.json styles and scripts, before the package's own files so those win; for a page app it prints the tags to add. The files then belong to the package: it stays self-contained, keeps working on a host without the library, and its author may change them. wardian add never replaces a file that is already there unless given --force. A running host shows every component at /ui/.

Every app SHOULD use the library, so all apps look and work alike. wardian check warns about a suite or page app with no theme.css. A module app needs nothing: the host draws its functions with the library.

6.11. Arrange#

Every app offers Arrange: each viewer may reorder its panels, move them between two columns, hide them, or use one column. The layout belongs to the viewer and the host keeps it; the package never changes. A hidden panel's code still runs, so an app MUST NOT depend on where a panel sits or on being visible. All three kinds use the same script, ui/arrange.js:

Wardian keeps layouts, each app's storage data, the latest channel messages and the app list's folders in its data folder (state/), with a copy in the browser for a viewer the host does not let write (ADR-2610071055). wardian check warns about a page with no data-panel.

Folders (ADR-2610081830) are how the viewer files the apps in the host's app list; a package says nothing about them, and an exported file holds none. Wardian keeps them in state/folders.json: {v: 1, folders: [{id, name, open, apps: [package names]}], seeded, examples}. Folders hold apps, not folders; an app is in at most one; a name is 1 to 60 characters; there are at most 100. GET /api/state/folders returns the record after dropping names that match no app being served, and the first time it files the example apps present into a folder with the id examples, named "Examples", open only when every app served is an example; seeded then says this was done and examples lists the examples already filed, so an example new to the data folder goes into that folder while it exists. POST /api/state/folders with {folders: [...]} replaces the folders and returns the record as kept; seeded and examples are the host's. Both need the same rights as the rest of the viewer's state.

7. Distribution#

7.1. A folder. Put the package folder in the host's apps folder, or in the Google Drive folder the host serves. Wardian's apps folder is its working folder, DATA_DIR/apps, kept apart from any source repository; each save there is a version in the app's history (ADR-2610071122).

7.2. A .wardian file is a zip. It SHOULD hold one package folder at its top: my-app.wardian → my-app/app.wasm, my-app/app.json, … A .zip is read the same way. A host SHOULD also accept .rustle, the extension from before the rename. Its media type is application/vnd.wardian+zip, and on macOS its type identifier is studio.wardian.package.

A file a host exports (ADR-2610071248) holds exactly the files wardian check reads, and a manifest at <package>/.wardian/export.json: {format: 1, type, package, title, exported_at, wardian_version, includes: {app, data}, data: {storage, layout, tables: [{name, rows}]}}. With the user's consent it also holds the app's data under <package>/.wardian/data/: storage.json (its storage), layout.json (its Arrange layout) and tables.sqlite (its db tables). It MUST NOT hold keys, accounts, permission answers, history or another package's data. The folder is hidden, so a host that predates exports, and wardian check, ignore it. A host SHOULD show the manifest before importing, and MUST install the data only when the user asks; permissions are never imported.

7.3. Import is lenient. To accept project zips as they come, the importer also:

7.4. A saved web page (ADR-2610080905). Save as web page saves the app as the viewer sees it now as one HTML file, <app>-<yyyy-mm-dd>.html, that opens in any browser with no Wardian and no network. It is a rendering, not a program: HTML and CSS only, every visible panel in the viewer's Arrange layout, the current values of fields, and each canvas and chart as a data: image, under a note with the app's title, when it was saved, and that it is a copy that does not update.

Each frame renders itself, because only it can read its own document. A suite's kernel asks each visible panel's frame (6.4, 6.8); a page app is answered by a small script the host adds to the end of every page app's HTML it serves, which a page MAY steer by setting window.wardianSnapshot to a function that returns HTML text or an element (or a promise of one); a module app's cards are drawn by the host, so it copies them itself. The messages are {wardian: 'snapshot', k: 'ask', id} from the host and {wardian: 'snapshot', k: 'answer', id, html, css} (a suite's kernel answers with parts: [{name, slot, html}], css, mode and columns instead).

The answers are the app's own code, so the host MUST clean them in the browser, on inert parsed HTML, by an allow-list: no script, iframe, frame, object, embed, link, meta, base or form (a form becomes a plain box), no on… attribute, no javascript: or vbscript: URL, and no src, href, srcset or xlink:href but data:image/… sources and #fragment links. SVG is kept without script, foreignObject or animation. CSS loses @import, @font-face, url() to anything but a data: image, and expression(. Web fonts are left out. The file MUST start with

<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:">

so the browser blocks anything that gets through. A file over 25 MB is refused with a message that names the largest part.

7.5. The importer unpacks into a hidden staging folder first, so a failed import changes nothing. It refuses a package whose name is already taken, unless asked to replace it.

7.6. Removing. A host SHOULD move a removed package aside rather than delete it, so the removal can be undone. Wardian moves it to .trash/ inside the apps folder; hidden folders are never listed or served. A package a save replaces is not removed: its version stays in the history.

8. Starting and checking a package#

wardian new module my-app      numbers in, numbers out; Wardian builds the interface
wardian new page my-app        WebAssembly plus your own page
wardian new suite my-app       three sealed apps that talk through the kernel

Each template passes wardian check as created, and includes its Rust source and a build.sh.

wardian check my-app/          a package folder
wardian check apps/            every package in a folder
wardian check my-app.wardian    a zip, exactly as the importer would unpack it

The exit status is 0 if no package has errors and 1 otherwise. Warnings do not fail a check.

wardian check cannot run JavaScript, so it does not compare the contract in app.js with suite.json (6.4). The kernel does that when the suite starts.

To check files in an editor, map the schemas in your editor settings. For VS Code:

"json.schemas": [
  { "fileMatch": ["app.json"], "url": "./schemas/app.schema.json" },
  { "fileMatch": ["suite.json"], "url": "./schemas/suite.schema.json" }
]

9. Not in format 2#

This page is SPEC.md in the repository. Something wrong or missing? Change that file.