AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Small apps, sealed. On your machine.
Wardian keeps the small tools you make, often with AI, on a computer you control. It serves them
to your browser from a local folder or straight from Google Drive. Each app runs sealed: it cannot
see your files or your other apps unless you allow it, and the browser blocks its requests to
anywhere but its own package (three narrow routes stay open; /docs/security lists them). The full documentation is at /docs on a running Wardian.README 전문 보기 ↗Small apps, sealed. On your machine.
Wardian keeps the small tools you make, often with AI, on a computer you control. It serves them
to your browser from a local folder or straight from Google Drive. Each app runs sealed: it cannot
see your files or your other apps unless you allow it, and the browser blocks its requests to
anywhere but its own package (three narrow routes stay open; /docs/security lists them). The full documentation is at /docs on a running Wardian.
On macOS (Apple silicon or Intel) or Linux (x86_64 or aarch64, with glibc 2.39 or newer: Ubuntu 24.04, Debian 13, Fedora 40 and later), with no sudo:
curl -fsSL https://github.com/weave-registry/wardian/releases/latest/download/install.sh | sh
Then run wardian start --at-login: Wardian runs in the background, starts again each time you log
in, and opens http://127.0.0.1:8000 in your browser (wardian stop ends it; plain wardian runs it
in the terminal instead). The script
puts wardian in ~/.local/bin and the example apps in ~/.local/lib/wardian/example-apps,
checks the download against SHA256SUMS, and says the line to add if ~/.local/bin is not on your
PATH. It shows each step on one line with ✓ (or ✗ and why), in colour on a terminal, and ends
with what to run next. It never runs Wardian and never edits your shell files. Settings: WARDIAN_VERSION=0.4.0 installs that
version, WARDIAN_PREFIX installs somewhere else, and WARDIAN_DOWNLOAD downloads from another
folder holding the tarballs and SHA256SUMS (a mirror, or dist/ served by any web server).
To build from source instead, see Run below.
The name comes from the Wardian case, the sealed glass case that let living plants travel safely across oceans in the 1800s. Your apps are the plants; Wardian is the case.
Wardian was called rustle before version 0.4. The rustle command still works and runs
wardian, /sdk/rustle.js still loads, and .rustle files still import.
Start a new app with wardian new <module|page|suite> apps/<name>, then follow GUIDE.md.
The package format is defined in SPEC.md. Check a package with wardian check <folder or .wardian>.
Both documents are also served by Wardian at /docs, and the JSON Schemas at /schemas/.
An app is one folder. The folder name is the app's name (letters, digits, -, _, .).
It holds either a WebAssembly module (below) or a suite.json of cooperating apps (see Suites).
my-app/
app.wasm required: the WebAssembly module
app.json optional: title, description, page
index.html optional: the app's own page
... anything the page loads (js/, pkg/, css, images)
./app.wasm, ../pkg/app.js), because the folder tree is served as it is.app.json (every field is optional):
{
"title": "USL analyzer",
"description": "Fits the Universal Scalability Law to load test results.",
"page": "demo/index.html"
}
Without page, the host looks for index.html, then demo/index.html, www/index.html and web/index.html.
App pages run sandboxed. The browser gives each page a throwaway origin, so the page
cannot use the host's settings, read other apps' data or use localStorage. To keep data,
let the user download and upload it.
Folders called node_modules or target, hidden files, and names with other characters are not served.
For a Drive folder or a local folder, put one app folder per app inside it. For a zip, see "Import apps from a zip" below.
A suite is a folder with a suite.json. It holds several small apps that share one screen and
talk to each other, like the USL lab in apps/usl-lab. The host kernel runs it:
tests/run-page-sandbox-e2e.sh proves both).suite.json, not the contract in the app's code. An app
whose code declares a different contract is not started.retain is replayed to an app that starts listening late.An app is apps/<name>/app.js plus an optional apps/<name>/view.html:
Kernel.register({ name, emits, listens, provides, needs, caps, init(ctx) { ... } })
ctx has root, $, $$, el, text, emit, on, provide, call, store, cap,
observe, source and spawn. It matches the single-file USL lab kernel, so apps run in both.
suite.json:
{
"title": "USL scalability lab",
"styles": ["https://fonts.googleapis.com/css2?...", "core/style.css"],
"scripts": ["core/lib.js"], // inlined into every app frame
"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"] }
]
}
slot is aside (the sticky left column), main (the right column, in order) or absent (an app with no view).
wrap is the element the view sits in, so the suite's CSS applies as it did on one page.
Capabilities:
| Capability | What the app gets |
|---|---|
storage | ctx.store.get/set, kept by the host for each app, in its data folder |
worker | ctx.spawn(code), a Web Worker inside the app's frame |
source | ctx.source(id), the text of an inlined script, e.g. "engine-src" |
claude:downloads | ctx.cap("downloads") → save({filename, data}) saves a file |
claude:sample | ctx.cap("sample") → sample(prompt, opts) and sample.json(prompt, opts), through the Claude provider set up in Settings (Anthropic API or Amazon Bedrock), after the user allows it; null when none is set up |
splunk | ctx.cap("splunk") → status(), search({search, earliest, latest}), jobs(), wait(id), cancel(id); see Splunk |
db | ctx.cap("db") → the app's own SQLite database: query, page, insertRows, tables, readPage, searchInto; see Large tables |
Debug in the browser console on the suite page: Kernel.apps(), Kernel.trace(), Kernel.faults().
Faults also show in a box at the bottom of the page.
Test: tests/run-suite-e2e.sh runs the USL lab and a hostile suite (tests/fixtures/rogue) in
Chrome. It needs Node with the playwright package.
cargo run --release # serves DATA_DIR/apps on http://127.0.0.1:8000
cargo run --release -- /path/to/apps
In a terminal, Wardian prints a short block and opens the address in your browser:
◆ Wardian 0.4.0
Small apps, sealed. On your machine.
Ready at http://127.0.0.1:8000
Apps ~/Library/Application Support/Wardian/apps · 5 example apps added
Admin this computer only
Press Ctrl-C to stop · log: ~/Library/Application Support/Wardian/wardian.log
The details (who counts as an admin, the data folder, where apps come from, the start and stop
records) go to wardian.log in the data folder. --no-open or WARDIAN_NO_OPEN=1 keeps the
browser closed; NO_COLOR=1 or TERM=dumb turns colour off. When the output is not a terminal (a
script, a service, the desktop launchers), Wardian prints the detail lines instead (admin: …,
data: …, listening on http://…) and never opens a browser. scripts/preview-terminal.sh
shows the block from a throwaway first start without touching your data. Without ADDR, a second
wardian finds the one already running (it answers /api/status), opens it and exits; if another
program holds port 8000, Wardian takes the next free port up to 8010 and says so. With ADDR set,
a busy address is an error.
Wardian serves and saves apps in its working folder, DATA_DIR/apps. Without DATA_DIR, the
data folder is ./data when Wardian starts in a Wardian checkout or where ./data already exists;
anywhere else it is ~/Library/Application Support/Wardian on macOS and $XDG_DATA_HOME/wardian
(default ~/.local/share/wardian) on Linux. Wardian prints the one it uses at start.
On each start it adds every example app the working folder has not had before: ./apps in a
checkout, otherwise the ones installed beside the program (../lib/wardian/example-apps, or
../Resources/apps in Wardian.app), otherwise the copies built into the program itself, so every
Wardian has them. It leaves out build output (target/, node_modules/, Cargo.lock); an example
you remove stays removed. After that, apps made or changed inside
Wardian (Make an app, Change this app, imports, restores) change only the working folder, never the
repository. To ship one of them, copy it back and commit it:
wardian promote splunk-table # copies DATA_DIR/apps/splunk-table into ./apps
git diff -- apps/splunk-table
Naming a folder (wardian /path/to/apps) serves that folder as it is. When it is inside a git
repository, Wardian says so at start, since changes made in the app then show up in git.
A Wardian you keep running should not run from target/release while you work on Wardian itself:
each cargo build replaces that file, and macOS can stop a program whose file was replaced under it.
Install a copy instead, with cargo install --path . (then run wardian), or copy
target/release/wardian somewhere else and run the copy. On macOS, remove the old copy before
copying a new one over it (rm -f bin/wardian && cp target/release/wardian bin/): a program file
rewritten in place keeps its old signature record, and macOS kills it when it starts.
Plain wardian runs until you press Ctrl-C. To keep Wardian running without a terminal, hand it to
the system:
wardian start # runs it now, as a service; opens it in your browser
wardian start --at-login # and also each time you log in
wardian status # whether it runs, how, where, and whether it starts at login
wardian stop # stops it, and its start at login
On macOS start hands Wardian to launchd as a user agent, studio.wardian; on Linux with a
systemd user session, to systemd as wardian.service. Neither needs sudo. The service runs the
program start was run with, by its full path, with the data folder written in full (found by the
rule above, from where you run start), WARDIAN_NO_OPEN=1, and its output appended to
wardian.log in the data folder. It comes back after a crash, but not after wardian stop. The
service file is ~/Library/LaunchAgents/studio.wardian.plist when it starts at login and
~/.config/wardian/studio.wardian.plist when it does not (launchd loads every file in
LaunchAgents at login), or ~/.config/systemd/user/wardian.service; wardian stop removes it.
Moving the program means running wardian start again. start without --at-login keeps a
start at login set before.
The service gets ADDR, HOME, XDG_CONFIG_HOME and WARDIAN_MASTER_KEY_FILE when they are
set, never a key: keep keys in Settings → Keys. Without launchd or systemd, start runs Wardian
detached, keeps its process id in wardian.pid in the data folder, and says that it will not come
back after a crash or start at login.
start waits up to 15 seconds for Wardian to answer, then opens it and prints:
◆ Wardian started 0.4.4
Ready at http://127.0.0.1:8000 · opened in your browser
Apps ~/Library/Application Support/Wardian/apps
Runs as a launchd service · starts at login
Stop it with wardian stop · log: ~/Library/Application Support/Wardian/wardian.log
When this same Wardian already answers, start opens it instead; another Wardian on port 8000 is
named and left alone. status prints the same kind of block, and exits 0 when Wardian runs and 3
when it does not. Outside a terminal, all three print name: value lines instead (running: yes,
address: http://…). The installer restarts a Wardian service when it replaces the program, so the
new version runs.
The server asks only for read-only Drive access. It re-reads the folder every REFRESH_SECS.
It keeps each file by its Drive checksum, so it downloads an unchanged app once and fetches a changed one again.
On an app's page, press Download. Wardian saves the app as a .wardian file that anyone with
Wardian can import: mail it, share it in chat or put it on Drive. The file holds the app exactly as
wardian check reads it. Tick Include my data to add what the app has saved, its layout and its
tables; Wardian lists what goes in first, because anyone you send it to can read it. Keys, accounts,
permission answers and history are never included. From the command line:
wardian export <app> [<file>] [--with-data].
When you import a .wardian file, Wardian first shows what it holds: the app, what it may use, and
any data. Its data is installed only when you tick Also install its data; the data it replaces is
kept in the app's history folder. Permissions are always asked again.
On an app's page, press Save as web page, then Save. Wardian saves what you see as one
.html file, <app>-<date>.html, that opens in any browser, offline, with no Wardian: every panel
you have not hidden with Arrange, in your layout, with what you typed, the results and each chart
as a picture. It is a copy that does not update: sorting, paging and buttons do nothing in it.
Everything on screen goes into the file, so check it before you send it. The file holds no script
and makes no request, so it is safe to open. Anyone who can open the app can save it this way; an
app may put more in the file than it shows, such as every row of a table (SPEC.md 6.4, 7.4).
In Settings → Import from a zip, choose a zip on your computer or paste a link to one. A Drive file link works too. The server fetches it with the service account, so share the file with that account. The apps are copied into the local apps folder.
The import keeps each app's folder tree, so a whole project zip works. For each .wasm, the app's top folder is:
app.json, orpkg/, dist/, build/, out/, ...).So usl-wasm/pkg/usl_wasm.wasm gives the app usl-wasm, with demo/, pkg/ and the rest kept in place.
If that top folder has no app.wasm, the module is also copied there under that name.
A .wasm at the very top of the zip gives an app named after the zip (or after the file, if it is not app.wasm).
A folder with several .wasm files and no app.wasm is skipped, because the import cannot tell which one is the app. The import refuses an app that already exists unless you tick Replace.
Limits: 100 MB per zip (64 MB from Drive), 64 MB per file, 256 MB unpacked in total.
Links must be public internet addresses. The server refuses links to itself, to cloud metadata
(169.254.169.254) and to other internal addresses, including after a redirect.
To import from a file server on your own network, set IMPORT_ALLOW_LAN=1.
Open the app and press Remove app, then confirm. The app moves to .trash/ in the working folder,
and nothing is deleted. Press Undo right away, or restore it later in Settings → Removed apps.
To delete removed apps for good, empty that .trash/ folder yourself. Apps served from Google Drive are removed in
Drive: Wardian only reads Drive.
Inside a suite, apps talk through topics (above). Separate packages can talk too, through channels, but only with your permission. A package declares the channels it wants in its own files. The first time it uses one, Wardian asks:
🍂 loan-planner wants to send messages on the channel budget. Allow · Don't allow · Not now
Wardian keeps your answer. Settings → App permissions lists every answer, and Revoke takes one
back. Wardian carries every message and stamps it with the sender's name, so no app can pretend to
be another. Messages travel between Wardian tabs in your browser, and the latest one on each channel
is kept for apps that open later. A package that uses channels needs "format": 2. See
SPEC.md §6.9 for the details.
Press Make an app, and say in your own words what the app should do. Claude writes the app, wardian checks it, and the app appears in your list. To change an app later, open it and press Change this app.
First add an Anthropic API key in Settings → Claude. Wardian tests the key, then keeps it on the server, sealed. The key never goes back to the browser. Each app uses some API credit on that key. If your key is not scoped to one workspace, also give the workspace ID (Console → Settings → Workspaces).
Settings → Claude → Models and limits chooses the models and the limits of Make an app and of
apps that use Claude. Settings → Usage shows the tokens each app used per day and sets its daily
cap (200,000 by default); past it, the app's claude:sample fails with over_budget.
Settings → Keys lists every key and account Wardian holds: the Anthropic key, Amazon Bedrock,
Splunk, the Google Drive service account and the admin token. Each shows where it came from and its
last test, with Test again and Remove. Wardian seals every saved key with AES-256-GCM under a
master key kept outside the data folder, in ~/.config/wardian/master.key (or where
WARDIAN_MASTER_KEY_FILE says), so a copy of the data folder holds no readable key. Back the master
key up apart from the data folder:
wardian key # where it is, and how many saved keys it opens
wardian key export ~/backup/wardian.key
wardian key import ~/backup/wardian.key # refuses a key that opens none of them
Claude on Amazon Bedrock. If you reach Claude through AWS, choose Amazon Bedrock in the same
card (ADR-2610071106). Give the region and either a Bedrock API key or AWS access keys (with a session
token for temporary credentials). Wardian tests them with one tiny request to the quick model before
saving them to data/bedrock.json, private like the other keys, and never sends them back to the
browser. The models must be enabled for your account in that region (Bedrock console → Model
access); otherwise Wardian says so. By default it uses the US inference profiles of Claude Sonnet 4.5
(builds apps) and Claude Haiku 4.5 (quick requests); set WARDIAN_BEDROCK_MODEL and
WARDIAN_BEDROCK_QUICK_MODEL for another geography (eu., apac., global.) or a newer model.
AWS profiles, SSO and instance roles are not read; give keys.
You do not have to wait on the chat. Claude works on the server, so you can open other apps, or close the tab, while it builds. The Make an app button shows how it is going: working, testing, ready or needs you. Any open Wardian tab tries each new save in a hidden frame and sends the errors back to Claude, and every tab in this browser finds the same chat.
How it works:
wardian check. A package with errors is not saved, and Claude
gets the errors to fix.Claude cannot compile anything here, so the apps it makes are plain JavaScript, HTML and CSS: a
page app (Wardian adds the empty app.wasm that marks the folder as an app) or a suite. An
AI-made app runs in the same sandbox as any other app.
Chats live in the server's memory. A restart ends them, but the apps they saved stay.
Press Arrange at the bottom left of any app to change its layout for yourself. Drag panels or use
their buttons, move them between the columns, hide the ones you do not need, or use one column.
Wardian keeps your layout in its data folder, so a restart, another browser or cleared site data does
not lose it; the app itself never changes. Reset
brings back the app's own layout. Suites and module apps get Arrange from Wardian. A page app marks its
parts with data-panel and runs wardian add arrange (SPEC.md 6.11).
The app list files apps in folders, one level deep. Apps in no folder come first, then each folder with its name and how many apps it holds; press a folder to open or close it. The first time, the built-in example apps go into a folder named Examples, closed if you have apps of your own and open if not; an example added by a later version goes there too while that folder exists.
New folder under the filter makes one. Each folder's ⋯ button renames it, moves it up or down, or deletes it; deleting a folder puts its apps back in no folder and never removes an app. Each app's ⋯ button is Move to…: no folder, any folder, or a new one. You can also drag an app onto a folder, drag it onto the box that covers the filter while you drag to take it out of its folder, and drag folders into order. Inside a folder, apps are in order of title. Typing in the filter opens the folders that hold a match until the filter is empty again.
Folders belong to the data folder, like layouts: Wardian keeps them in state/folders.json, an
exported .wardian file holds none, and two people sharing one Wardian share one set. An app that
is removed leaves its folder; a new app is in no folder.
Wardian has a small library of interface parts: button, field, card, badge, table, switch, tabs, dialog, toast, tooltip, progress, display type, glass, bento tiles and the share receipt. Like shadcn, you copy them into your app, and they become your code:
wardian add button tabs toast apps/my-suite # copies into apps/my-suite/ui/ and updates suite.json
wardian add --list
Open Components at the top of Wardian's app list (or /ui/) to see each one live, in light
and dark, with its markup. One file, ui/theme.css, sets the colours, corners and spacing for all
of them. The progress bar, <wardian-progress>, is also built into every app, so it needs no copy.
Apps that declare the splunk capability can run Splunk searches. The app never sees the
Splunk address or credentials: Wardian runs the search with one account that you set up.
https://<host>:8089, and the token (or a username and password).The first time an app searches, Wardian asks you, the same way it asks about channels. Your
answer is listed under App permissions, where you can revoke it. The server checks the answer
again on every search, and also checks that the app declares splunk in suite.json.
const splunk = await ctx.cap('splunk'); // null in a host without Splunk
const { ready } = await splunk.status(); // false until Settings → Splunk is done
const { fields, rows, truncated } = await splunk.search({
search: 'index=loadtest | stats avg(tput) AS x BY concurrency | table concurrency x',
earliest: '-7d', // Splunk time modifiers; '' means all time
});
fields is in the order the search names them; fields that start with _ are left out.
Each row is a list of values in that order. At most 10,000 rows come back; truncated says
when there were more. A search that does not start with | or search gets search added.
Splunk searches run only for an admin (see the next section): from a browser on this machine,
or with ADMIN_TOKEN.
A search over weeks of data can take minutes, so Wardian runs Splunk searches, loads into a table and Claude requests as background jobs on the server (ADR-2610072118). The app's promise works as before, but no browser connection waits on it: you can open other apps meanwhile, and leaving the app does not stop the search. The Jobs button next to Make an app shows how many are running, and its list shows each one's app, search, rows loaded and time, with Cancel. When a job finishes while its app is closed, the button says so; press it to open the app. The Splunk table app shows the rows loaded so far, has a Stop button, and when you open it again picks up a load that is still running or shows the one that finished.
const jobs = await splunk.jobs(); // this app's jobs of the last hour, newest first
const running = jobs.find(j => j.state === 'running' && j.kind === 'splunk.into');
if (running) { const big = await splunk.wait(running.id); /* the same answer searchInto gives */ }
await splunk.cancel(id); // also cancels the search on the Splunk server
Jobs are kept in memory for an hour, at most 50 per app; restarting Wardian forgets them. A script
that calls /api/splunk/search, /api/db/search-into or /api/ai/sample itself can still wait
for the answer; with "background": true it gets {job} at once and reads /api/jobs/<id>.
An app that declares db gets its own SQLite database, kept by Wardian in DATA_DIR/db/<app>.sqlite
(ADR-2610071219). Use it for data: rows you page, sort and filter. Keep storage for small settings.
const db = await ctx.cap('db');
await db.insertRows({table: 'runs', columns: ['n', 'x'], rows: [[1, 980], [2, 1900]], create: true});
const page = await db.page({table: 'runs', offset: 0, limit: 100, orderBy: 'x', desc: true, where: 'n > ?', params: [1]});
// page = {columns, rows, total, offset}
const big = await db.searchInto({search: 'index=web | table host status ms', earliest: '-24h', table: 'search'});
// loads up to 1,000,000 Splunk rows, read in chunks of 50,000; big = {table, columns, fields, total, truncated}
The Splunk table app works this way: a search loads into its database, the page shows 100 rows at a time, and sorting, Find in results and the CSV run in the database. It sends other apps a reference to the table instead of the rows; the USL lab reads the pages it needs (up to 10,000 rows for a fit) after you allow it once to read the Splunk table app's tables.
Apps write their own SQL, so Wardian keeps each one inside its file: it refuses ATTACH, loading
extensions, and pragmas that change settings; another app's tables can only be read, and only with
your permission; a database may hold 1 GB, and a statement that runs longer than 10 seconds is
stopped.
With no ADMIN_TOKEN, every program and browser on the same machine is an admin: it can change
settings, import and export apps, and browse Drive. That is fine for one person on a laptop, so
Wardian then listens on this machine only: ADDR must be a loopback address (127.0.0.0/8, ::1
or localhost). Asked to listen anywhere else without a token, Wardian refuses to start and says
to set ADMIN_TOKEN.
Set ADMIN_TOKEN to a long random string to listen on other addresses, or save a token in
Settings → Keys (ADMIN_TOKEN wins). Then whoever sends the token is an admin, and nobody else
is, this machine included. Settings asks for the token.
At start Wardian prints who counts as an admin.
Docker needs the token, because it listens on 0.0.0.0 and its requests do not come from the
same machine.
ADMIN_TOKEN=<long random string> docker compose up -d --build
Docker keeps the master key that seals saved keys on a second volume, keys. Back it up apart
from data.
Settings live in DATA_DIR (in a checkout ./data, which git ignores; see Run for the rule).
config.json — the chosen source and Drive folderservice-account.json — the uploaded key, sealedanthropic-key — the API key for Make an app, sealedbedrock.json, admin-token — the Bedrock keys and the admin token saved in Settings, sealedagent.json — Claude's models, limits and daily capsusage.json — the tokens Claude used, by day and app, for 31 dayskey-checks.json — the last test of each key, without the keygrants.json — your answers to channel and Splunk permission questionssplunk.json — the Splunk address and account, sealedapps/ — the working folder: the apps Wardian serves and saves; apps/.examples-seen lists the
example apps it has been givenEvery file is readable by its owner only. Sealed files open only with the master key, which is not
in DATA_DIR.
wardian.log — every start and stop of the server, with the time in UTC: Ctrl-C, SIGTERM, a
closed terminal (SIGHUP, caught only when stderr is a terminal), a panic, an address already in
use. A start with no stop before it means the previous run was killed (SIGKILL, out of memory,
the machine stopped). Past 1 MB it moves to wardian.log.1.history/<app>/ — each app's versions, with log.json saying when, by what and whystate/ — layouts, apps' saved data, the latest channel messages and the app list's foldersdb/<app>.sqlite — each app's own database (the db capability), readable by its owner only| Variable | Default | Meaning |
|---|---|---|
ADDR | 127.0.0.1:8000 | Address to listen on; anything but 127.0.0.0/8, ::1 or localhost needs ADMIN_TOKEN. Unset, a busy 8000 moves Wardian up to 8010, or opens the Wardian already there |
WARDIAN_NO_OPEN | unset | 1: a start in a terminal does not open the browser (as --no-open) |
NO_COLOR | unset | Any value: no colour in the terminal output |
WARDIAN_SERVICE_LABEL | studio.wardian | The service wardian start, stop and status manage (the systemd unit drops studio.); tests use their own, starting studio.wardian. |
DATA_DIR | data in a checkout or where it exists, else the platform's folder (see Run) | Where settings, the working folder of apps and their history are kept |
ADMIN_TOKEN | none | Whoever sends it is an admin, and nobody else; wins over a token saved in Settings; a non-loopback address needs one of the two |
WARDIAN_MASTER_KEY_FILE | ~/.config/wardian/master.key | The file holding the master key that seals saved keys; made on the first start |
WARDIAN_MASTER_KEY | none | The master key itself, 64 hex digits, instead of a file |
REFRESH_SECS | 60 | How often to re-read the Drive folder |
GDRIVE_FOLDER_ID | none | Start on this folder; wins over the saved one |
GDRIVE_SA_KEY | none | Key file path, used only if no key was uploaded |
IMPORT_ALLOW_LAN | off | 1 lets zip links point at the local network |
ANTHROPIC_API_KEY | none | API key for Make an app, used only if none is saved in Settings |
cargo test --release
tests/run-all.sh # everything, one after another: unit tests, hexa (if installed), every browser suite
Each tests/run-*-e2e.sh starts its own Wardian with a throwaway data folder, fakes for Splunk,
Claude and Bedrock where it needs them, and drives the pages in a browser.
tests/run-install-e2e.sh needs no browser: it builds a release tarball, installs it with
install.sh from a local web server into a throwaway home folder, starts it, and checks that a
tampered tarball is refused. They need python3 and
Node with the playwright package (npm i -g playwright). They launch Google Chrome; set
WARDIAN_BROWSER=chromium to use Playwright's own Chromium instead (npx playwright install chromium), as CI does. tests/run-all.sh stops at the first failure.
CI (.github/workflows/ci.yml) runs the fast checks (unit tests, hexa, the install test) on every
push to main; every browser suite at once, one runner each, on pull requests, before each release
and by hand; and the load test every night (ADR-2610082000). A release builds nothing until the full
set passes. CI downloads hexa from its GitHub release, at the
version HEXA_VERSION in ci.yml names, and checks it against its checksums: the fast checks run
hexa analyze . --grade A and hexa adr doctor, and the full set runs hexa adr gates.
The suites above use fakes. tests/live/ checks Wardian against the real services: Bedrock by API
key and by SigV4, a Splunk search of at least 100,000 rows loaded and paged, a Google Drive folder
listed and an app opened from it, and an export just under the 100 MB limit imported into a second,
empty Wardian. Each check runs only when its credentials are set and prints SKIP otherwise; the
export check needs none.
AWS_BEARER_TOKEN_BEDROCK=… AWS_REGION=us-east-1 \
SPLUNK_URL=https://splunk:8089 SPLUNK_TOKEN=… \
GDRIVE_SA_KEY=~/keys/wardian.json LIVE_GDRIVE_FOLDER_ID=… \
tests/live/run.sh
Every check starts its own Wardian on 127.0.0.1:0 with a throwaway data folder, hands it only the
variables it names, and uses the same HTTP API as the browser. tests/live/README.md
lists each check's variables and what it proves. Run them once per release.
On main, with everything committed: tests/run-all.sh passes, on a machine with hexa
(hexa analyze . --grade A and hexa adr gates).
tests/live/run.sh with every credential you have. Its last lines (PASS / SKIP / FAIL
per check) go into the release's section of CHANGELOG.md, so the notes say which ran.
Move the [Unreleased] entries in CHANGELOG.md under the new version and date, and set the
same version in Cargo.toml (cargo build updates Cargo.lock). Commit.
Build the packages, which land in dist/:
scripts/package-macos.sh # dist/Wardian.app and Wardian-<version>-macos-<arch>.zip
scripts/package-linux.sh # dist/wardian-<version>-linux-<arch>-desktop.tar.gz
Without credentials the macOS bundle is signed ad hoc: it runs on the Mac that built it, and
Gatekeeper refuses it elsewhere. To sign it, set DEVELOPER_ID to a Developer ID Application
identity (security find-identity -v -p codesigning); to notarize it too, set NOTARY_PROFILE
(from xcrun notarytool store-credentials) or APPLE_ID, APPLE_TEAM_ID and
APPLE_APP_PASSWORD. Both need an Apple Developer account. The bundle's Info.plist declares the
type studio.wardian.package for .wardian files (application/vnd.wardian+zip), so Finder
shows them as Wardian apps and opens Wardian; installing still goes through Import, which shows
what a file holds first. On Linux, install.sh in the tarball registers the same MIME type and
a desktop entry.
Tag and push to both remotes:
git tag -a v1.0.0 -m "Wardian 1.0.0"
git push origin main v1.0.0 && git push upstream main v1.0.0
On GitHub the tag starts .github/workflows/release.yml: it builds the four tarballs the
one-line install uses (macOS arm64 and x86_64, Linux x86_64 and aarch64, each with
scripts/release-tarball.sh), writes one SHA256SUMS for them, and publishes the release
with them and install.sh. The tag must match version in Cargo.toml.
Add the zip and the desktop tarball to that release, and put the version's CHANGELOG.md
section in its notes.
MIT. See LICENSE. The components in static/ui/ are under the same license, so apps
that copy them in with wardian add may use them freely.
SPLUNK_URL| none |
| Splunk management address, used only if none is saved in Settings |
SPLUNK_TOKEN | none | Splunk token (or set SPLUNK_USERNAME and SPLUNK_PASSWORD) |
SPLUNK_INSECURE_TLS | off | 1 accepts any certificate, such as Splunk's self-signed default |
SPLUNK_CA_FILE | none | PEM file of extra certificate authorities to trust for Splunk |
WARDIAN_SPLUNK_API_PORT | 8089 | The port Settings → Splunk tries for Splunk's API after the address given |
WARDIAN_AI_MODEL | claude-opus-5-5 | The Claude model that writes apps; one chosen in Settings wins |
WARDIAN_AI_PROVIDER | anthropic | bedrock to use Amazon Bedrock, used only if none is chosen in Settings |
AWS_REGION | none | Bedrock region (also AWS_DEFAULT_REGION) |
AWS_BEARER_TOKEN_BEDROCK | none | Bedrock API key; or AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN |
WARDIAN_BEDROCK_MODEL | us.anthropic.claude-sonnet-4-5-20250929-v1:0 | The Bedrock model that writes apps |
WARDIAN_BEDROCK_QUICK_MODEL | us.anthropic.claude-haiku-4-5-20251001-v1:0 | The Bedrock model for quick requests |
GDRIVE_API_BASE | For tests only |
ANTHROPIC_BASE_URL | Anthropic | For tests only |
WARDIAN_BEDROCK_BASE_URL | the region's endpoint | For tests only |