Review pages for coding agents.
- The agent writes a short OpenUI Lang program, not HTML.
- You open it in the browser, click any part, and write a note.
- The agent gets each note with the name of the statement it points at, edits only those statements, and the page updates in place.
The real loop, about 30 s: MP4 · WebM.
Website · Playground (no install) · Gallery · Benchmark · Writing a pack
root = Stack([intro, flow, options, decide], "column", "l")
intro = Card([CardHeader("Session storage migration", "Redis → Postgres"), introText])
introText = MarkDownRenderer("Sessions are lost on failover. **Goal:** durable sessions, no forced logout.")
flow = Card([CardHeader("Request path"), flowDiagram])
flowDiagram = Architecture([client, api, pg], [toApi, toPg])
client = Node("Client", "external")
api = Node("API", "backend", "Node :8080")
pg = Node("Postgres", "database", "sessions table", "new")
toApi = Edge(client, api, "HTTPS", "emphasis")
toPg = Edge(api, pg, "SQL")
options = Card([CardHeader("Rollout options"), optionTable])
optionTable = Table([Col("Option", ["Big bang", "Dual-write"]), Col("Risk", [Tag("high", null, "sm", "danger"), Tag("low", null, "sm", "success")])])
decide = Form("rollout", Buttons([Button("Send decision", @ToAssistant("Decision: rollout"), "primary")]), [strategyField])
strategyField = FormControl("Rollout", RadioGroup("strategy", [RadioItem("Big bang", "", "bigbang"), RadioItem("Dual-write", "", "dualwrite")]))
A full page (KPIs, diagram, table, chart, diff, decision form) is about 1,000 tokens: examples/session-migration.html.
See it without installing: the gallery shows example pages next to their source and token counts. The playground lets you edit OpenUI Lang and see the page live.
Needs Node.js 22.18 or later. Your agent runs:
npx -y austere-axi spec # component reference (read once per session)
npx -y austere-axi plan.html # validate and open the page for review
npx -y austere-axi poll plan.html # wait until the reviewer presses Send--help prints the file skeleton and the loop (about 900 tokens); help all adds the rarer commands and the environment. The agent does not look at the page itself: the reviewer judges how it looks, and render failures come back through poll. To have agents reach for it on their own, use the skill, plugin or MCP server.
Same loop as lavish-axi. The difference: the agent never writes HTML, CSS or layout.
| lavish-axi | austere-axi | |
|---|---|---|
| Agent writes | Full HTML + CSS + SVG | OpenUI Lang statements |
| After feedback | Edits the HTML; the page reloads | Edits the named statements; the page patches in place |
| Feedback points at | CSS selector + text | Statement name and line, plus table cell, diff line, diagram node or selected text |
| Validation | Layout audit in the browser | austere-axi check, in Node, no browser |
| Look | Different for each agent | One dark design |
Fewer output tokens: faster pages, lower cost. From the benchmark:
Across 36 paired runs (6 review pages, 2 models), lavish-axi spent a median 3.2× the output tokens of austere-axi to produce the same page, took 2.4× as long and cost 2.5× as much. Answering one reviewer note took 2.7× the output tokens.
lavish-axi does better in some cases. Read the benchmark caveats before you quote the numbers.
Needs Node.js 22.18 or later.
Let your agent do it. Install the setup skill, then ask your agent to "set up austere-axi":
npx skills add MandalorianBatman/austere-axi --skill austere-setupIt checks Node, installs the austere skill, offers the optional items below, and opens a test page.
Or do it yourself. Agents can run the CLI with no install (-y skips the npm prompt):
npx -y austere-axi --helpOptional:
npm install -g austere-axi # `austere-axi` command, no npx
npx skills add MandalorianBatman/austere-axi --skill austere # tells agents when to make a page; adds /austereThe austere skill only points at --help and spec, so it never goes stale. A global install stays on its version until npm update -g austere-axi.
Bundles the austere skill (and /austere), the austere-setup skill and the MCP server:
claude plugin marketplace add MandalorianBatman/austere-axi
claude plugin install austere@austere-axiIn a session: /plugin marketplace add MandalorianBatman/austere-axi, then /plugin install austere@austere-axi. The MCP server runs npx -y austere-axi mcp (needs Node 22.18+ and npm access). claude plugin update austere updates the skill text; the CLI comes from npm each time.
austere-axi mcp is an MCP server on stdio. Tools: spec, check, open, poll, reply, patch, end, components. Each one runs the CLI command of the same name, so the loop is the same as on the command line.
pollreturnsstatus: timeoutafter 50 s by default, because many clients cancel longer calls. The agent polls again; nothing is lost.timeout_ms: 0waits until feedback arrives.pollandreplytake thecursora poll returned, like--cursor.componentstakescommand(search,show,listoradd) andargs;showalso takespack, like--pack.addtakes only an npm package name, never a folder or.tgz, and has notrust: a third-party pack the user has not trusted is refused, and the agent asks the user to install it.- Pass absolute file paths. The server resolves relative paths against its own working directory, which depends on the client.
- No screen? Set
AUSTERE_AXI_NO_OPEN=1in the server'senvsoopenprints the link instead. Other environment variables work the same way.
Claude Desktop (claude_desktop_config.json) and Cursor (~/.cursor/mcp.json, or .cursor/mcp.json in a project):
{
"mcpServers": {
"austere": {
"command": "npx",
"args": ["-y", "austere-axi", "mcp"]
}
}
}Codex (~/.codex/config.toml), or codex mcp add austere -- npx -y austere-axi mcp:
[mcp_servers.austere]
command = "npx"
args = ["-y", "austere-axi", "mcp"]agent writes plan.html ──► austere-axi plan.html ──► local server + browser review
│ reviewer annotates, picks options,
│ types, presses Send
agent patches changes ◄── austere-axi poll plan.html ◄─┘ (long-poll returns the batch)
│
└─► page updates in place and flashes the changed statements
- A page is a plain
.htmlfile: one runtime<script>tag and one<script type="text/openui">block. Opened directly, it loads the runtime from the CDN. Under review, the server serves a local copy. - Components: 58 stock OpenUI components, plus
Architecture(Node,Edge,Group),DiffandFileTree. Nine shadcn-style components come as a bundled pack. - Diagrams lay out automatically (dagre); the agent never writes coordinates. Every node and edge is a click target.
lanesputs each group in a column;tonecolors a box or line to show risk. - Notes attach to the smallest named statement under the click, with detail where it exists: table row and column,
file:+linein a diff, a diagram node, or selected text. The reviewer can move a note to a parent section. - Changes flash after each agent turn, and the bar shows
N changed. Click it to keep the last turn's changes outlined until you click again. - Buttons and forms send feedback.
@ToAssistant("…")sends a message with the form values. A button with no action sends its label. - Phone review: with Tailscale running, the server also listens on the tailnet IP and prints a phone link. That link carries your owner key; keep it private. The review UI has a mobile layout.
Usage errors (unknown command or flag, missing argument) exit 2 before anything runs; failures exit 1. <command> --help prints that command's flags and an example.
austere-axi sessions + command list
austere-axi <file> validate + open a review [--no-open] [--reopen]
austere-axi poll <file> long-poll for feedback [--cursor n] [--agent-reply "..."] [--after n] [--takeover]
austere-axi reply <file> post a reply without polling [--cursor n]
austere-axi end <file> stop as the agent; the reviewer can still add notes
austere-axi close <file> close the review: no more notes until reopen
austere-axi reopen <file> reopen an ended or closed review
austere-axi login [file] single-use link that signs a browser in as you (SSH, --no-open)
austere-axi feedback <file> feedback no agent has answered yet [--all] [--peek]
austere-axi check <file> validate the program (no browser) [--fix] [--render: headless Chrome, text report]
austere-axi patch <file> merge statements from stdin into the program (reports dropped unreferenced ones)
austere-axi spec [group|pack...] component reference (~5.8k tokens; groups narrow it)
austere-axi components ... list | search | show | add | remove | new | build
austere-axi trust list|add|remove packs your agent may add without --trust (see Trust)
austere-axi registry list|add|remove private npm registries for scopes (see Private registries)
austere-axi export <file> one self-contained HTML file, works offline (~1.1 MB)
austere-axi hub on|off|status|list|notes|open home page with every page and note
austere-axi share <file> --as "<name>" invite link for a teammate on your tailnet [--auto]
austere-axi share list|revoke see invites, cut one off
austere-axi publish <file> hosted copy of the page on austere.kasat.xyz [--invite <name>]... [--expires <dur>] [--unlisted] [--auto] [--no-presence] [--new]
austere-axi publish invites|revoke <file> [name] see a page's invites, cut one off
austere-axi publish list|rm <file|id> your hosted pages; delete one now
austere-axi stop stop the background server
austere-axi server --persist run the server in the foreground and never idle out
austere-axi mcp MCP server on stdio (see MCP)
Output is compact YAML for agents. Each poll item has its statement and that statement's line and source, so the agent does not read the file again. Agents revise with patch only: they send the changed statements, never the whole file.
- One listener: one
polllistens on a session at a time. - No lost notes: every note is written to
~/.austere-axi/hub.dbbefore the page is told it was sent, so it survives a server crash or restart. Feedback sent while no agent listens waits for the next poll. - Acknowledge with the cursor:
pollprints acursor. Passing it back (--cursor non the nextpollorreply), or replying at all, acknowledges everything up to it. Without it,pollreturns the same feedback again, so a crash or timed-out tool call loses nothing.--after nreplays from any point. - End is soft: ending a review (the agent's
end, or End review in the page) means the agent stopped. The reviewer can still send notes. The nextpollreturns every unacknowledged note, including ones sent before the end or released from hold after it, and reopens the review. A batch the reviewer sent together with end stays final. - Close is hard: only the owner's close (
close <file>, or Close review in the page) locks the composer.reopen <file>or Reopen review unlocks it. - A review the user ended or closed opens again only with
--reopen. - An
Imagesrc that is a path relative to the page (png, jpg, gif, webp, svg, avif) loads under review for the owner and for invited reviewers, andexportinlines it as a data URI. Both read only regular files in the page's own folder, never dotfiles or links that leave it;exportskips an image over 10 MB. A fileexportcannot read stays as written, and the output names it innotices. - Tabs closed:
pollreturnsbrowser_disconnected10 s after the last review tab closes, or 5 min after a dropped connection (for example, a locked phone). The review stays open, so later notes reach the nextpoll. feedback <file>returns notes no agent has answered, from any day. Reading them acknowledges them;replymarks them answered.- Render failures reach the agent without a click: an unknown component or a component that throws. The owner's open review page sends them as a note from "Render check", once per page revision, and
pollreturns them asrender_erroritems. With no review tab open, nothing renders, so nothing is sent.check --render <file>renders the page in headless Chrome (an installed Chrome, Chromium or Edge, orCHROME_PATH) at 1280 and 390 px, opens each tab in turn, and prints render errors and the statements that make the page scroll sideways, as text, with no screenshot. Agents run it only when the user asks for a check.
spec covers the common components (~5.8k tokens). Rare ones (area and scatter charts, sliders, switches, editable tables, …) are in the library but not in spec. Find them with components:
austere-axi components # every component, by group
austere-axi components search scatter # search by name, group, description; also packs on npm
austere-axi components show AreaChart # signature, notes, example
austere-axi components show Milestones --pack austere-pack-orbit # one pack's, when several share the namePacks add components. A pack is an npm package with dist/pack.js (the components, a few KB) and dist/catalog.json (signatures, examples, schema).
austere-axi components add austere-pack-gantt --trust # npm name, folder, or .tgz
austere-axi check --fix plan.html # adds the pack tag to the pagecomponents search and components show also cover packs on npm that are not installed, meaning any package with the keyword austere-axi-pack. Search lists up to 8 core and installed components under results, then up to 3 from packs on npm under npm, so a pack on npm never pushes a core component out. A result names its pack, says not installed, and says whether the pack is first-party, trusted (on your trust list) or third-party; show cuts such a component's description, example and notes short. show <Name> returns the first component by that name, core and installed packs first; --pack <pack> looks only in that pack, and search names it for an npm result whose name another result or a local component shares. The CLI reads the pack's catalog from its tarball, checked against the registry's integrity hash, and runs none of its code. It caches the pack list for an hour, under ~/.austere-axi/npm-packs/. It caches each pack version for good once it is read, fails the install checks, or the registry answers 404 or 410 for it; any other failure is tried again on the next search. For a third-party pack, the agent is told to ask you to review it and run components add <pack> --trust. With the registry unreachable, search shows local results, says so, and tries the registry again after 5 minutes. A pack the registry fails to serve is left out, and search says how many were.
A page names its packs in the head, so it renders anywhere:
<meta name="austere-packs" content="[email protected] sha384-…">- Under review, the server serves an installed pack only while it matches its hash in
~/.austere-axi/packs.json. Opened directly, the page loads packs from jsDelivr with Subresource Integrity.exportinlines them. exportinlines the runtime and packs gzipped (base64), and a short loader unpacks them withDecompressionStream(Chrome 80, Firefox 113, Safari 16.4). Review and patch the source page; the exported copy is for sharing.checkflags an exported copy, and review serves it as it is, with no second runtime.- A pack component with no pack tag is a
missing-packerror.check --fixrepairs it. - Bundled packs need no tag and load only when used:
shadcn:Progress,Avatar,AvatarGroup,Tooltip,HoverCard,Breadcrumb(BreadcrumbItem),Kbd,Skeleton,CalendarandTimeline(TimelineItem). Read-only.
rollout = Progress(64, "Rows migrated") freeze = Calendar("2026-11", ["2026-11-04 Code freeze"])
Trust. Pack code runs inside your review pages, where notes and button presses also come from. Packs under @austere-axi/ on npm install directly. Other npm packs and any .tgz need --trust, so read them first. A local folder counts as your own. A page with third-party packs shows a warning, and poll tells the agent.
Your trust list, ~/.austere-axi/trusted.json, names npm packs your agent may install without --trust:
austere-axi trust add [email protected] # pins that version and the hash of its pack.js
austere-axi trust add @acme # every pack in a scope on the current registry, e.g. your company's
austere-axi trust list
austere-axi trust remove @acme- A pin trusts one exact version with the same
pack.js. A later publish, or the same version with other code, still needs--trust.components add <pack>with no version installs the pinned one. - A scope is bound to the registry it was added from: its private registry when the scope is mapped to one (see Private registries), else the default (
AUSTERE_AXI_NPM_REGISTRY, elsenpm_config_registry, else npmjs). Pointed at another registry, packs in that scope are third-party again, since anyone may own the same scope name there. - Search marks packs on the list
trustedand tells the agent it may add them. trust addasks you to confirm at a terminal and refuses when stdin is not one, which is how agents run commands. That stops an agent adding a search hit to the list by accident. It does not stop an agent set on editing the file.- Only this file in your home folder counts. A project cannot ship its own list.
- If the file is unreadable, search counts every npm pack third-party and says so, and
trustrefuses until you fix or delete it.components addrefuses only the installs the list would decide: a third-party npm pack without--trust. First-party packs, local folders and--trustinstalls are unaffected.
Private registries. Packs your team publishes to Verdaccio, Artifactory or GitHub Packages are found and installed once you map their npm scope to that registry, in ~/.austere-axi/registries.json:
austere-axi registry add @acme https://npm.acme.dev/ --token-env ACME_NPM_TOKEN
austere-axi registry list
austere-axi registry remove @acmecomponents searchandshowquery the default registry and each mapped one, with the sameaustere-axi-packkeyword search. A hit from a mapped registry names its host, e.g.npm @acme/[email protected] (npm.acme.dev), not installed, third-party. If one registry does not answer, the others still do and the result says which one was unavailable.components add @acme/<pack>installs from @acme's registry. A mapped registry counts only for its own scope: packs it lists under other names are ignored, and the default registry's packs in that scope are left out.--token-envnames an environment variable that holds a bearer token. The file stores the name, never the token. The token goes only to that registry's own origin, never to the default registry or to a tarball on another host.- The URL must be https, or http on localhost.
@austere-axicannot be mapped. registry addruns only at a terminal, liketrust add: a mapping decides which host receives the token, so an agent without a terminal is told to ask you.registry removeworks anywhere.- Mapping a scope grants no trust: its packs stay third-party until you trust the scope (
trust add @acme, which records the mapped registry) or pin a pack. - Only this file in your home folder counts. If it is unreadable, search covers the default registry only and says so, while
components add,trust addandregistryrefuse until you fix it.
Write a pack: Writing a pack goes from scaffold to npm publish, with austere-pack-gantt as the example. Short version:
austere-axi components new austere-pack-milestones
cd austere-pack-milestones && npm install && npm run build
austere-axi components add .Each component uses austereComponent from austere-axi/pack: a PascalCase name, zod props in positional order (append-only after you publish), a one-line description and an example. The build loads the pack in Node, so build only code you trust.
The server starts when needed and stops after 30 idle minutes. Every review is kept in ~/.austere-axi/hub.db (SQLite) either way. hub on adds a home page over it and indexes every page you check or patch:
austere-axi hub on # http://127.0.0.1:4388/: pages by project and day, with search
austere-axi hub list # the same, for an agent: [words] [--project dir]
austere-axi hub notes # every note, same for an agent: [words] [--project dir] [--unanswered]
austere-axi hub off # back to the plain session list; the database staysThe home page shows which reviews are live and how much feedback the agent answered. /notes lists every note, newest first, with search, status and project filters.
- Session files from before 0.5 (
~/.austere-axi/sessions/*.json) are imported intohub.dbon first start and renamed to*.imported. - One server runs per state dir; a second one refuses to start.
server.lockholds the running server's pid and port. If that server no longer answers on the port, the lock is stale and is taken over.
Run the server under a supervisor with --persist so it never idles out, and point the proxy at it:
# ~/.config/systemd/user/austere-axi.service
[Unit]
Description=austere-axi review server
[Service]
ExecStart=/usr/bin/env npx -y austere-axi server --persist
Environment=AUSTERE_AXI_LINK_URL=https://austere.example.com
Restart=on-failure
[Install]
WantedBy=default.targetThen systemctl --user enable --now austere-axi. CLI commands on the same machine use the running server. A newer CLI that replaces an older server also starts it with --persist. AUSTERE_AXI_PERSIST=1 is the same as --persist.
Read the trust model first: behind a proxy on the same machine, every request looks local.
Teammates on your tailnet can review a page and leave notes under their own name. The server still runs on your machine. No accounts, no hosted service.
austere-axi share plan.html --as "Ana" # prints a link; notes wait for you
austere-axi share plan.html --as "Bo" --auto # Bo's notes go straight to the agent
austere-axi share list [--links] # who has a link
austere-axi share revoke Ana # name or id; takes effect at onceSend the link to the teammate. It needs Tailscale (or AUSTERE_AXI_HOST); without one, share stops with an error.
- Hold (default): a teammate's note waits in your side panel. Send one note, Send all, or Discard. The agent does not see held notes (
pollandfeedbackskip them) until you send them. Up to 200 held notes per teammate; more are refused until you send or discard some. - Auto: the note goes to the agent without your approval.
- Author:
pollandfeedbackshowfrom: <name>on a teammate's note. Your own notes have nofrom. The hub Notes view shows the author and can filter on it. - Reviewers can read the page and the conversation, and add notes. They cannot end the review, export, or send held notes.
Trust model. Anyone with the link who can reach your machine on the tailnet can post notes under that name until you revoke it. Share only with people you trust, and revoke when the review is done.
- The link holds a random 192-bit token. Opening it shows a "Continue as " button. Pressing it (a same-origin POST) trades the token for a cookie and removes it from the address bar, so a link opened from another site cannot turn your browser into a reviewer.
- A request without a valid invite or the owner key is refused, on loopback too. The hub and its Notes view are yours only. A reverse proxy on the same machine gets no owner rights from looking local.
- Your own phone link carries the owner key (
~/.austere-axi/owner.key); do not forward it. To revoke all old phone links and owner cookies, delete that file and restart the server.
publish uploads a page to austere-cloud and prints one link per invited reviewer. Nothing is sent until you run it; after that, the local server keeps your published pages in sync (below).
austere-axi publish plan.html --invite Ana --invite Ben # prints https://austere.kasat.xyz/p/<id>#i=<token> for each
austere-axi publish plan.html # again: same page, new version; Ana's and Ben's links keep working
austere-axi publish plan.html --invite Cy --expires 7d # one more reviewer, for a week
austere-axi publish invites plan.html # who is invited (live, expired, revoked), with their links
austere-axi publish revoke plan.html Ben # Ben's link stops working; his open page is disconnected
austere-axi publish plan.html --unlisted # opt-in open link: anyone with the plain URL may review
austere-axi publish plan.html --new # a fresh page
austere-axi publish list # your pages, expiry, note counts
austere-axi publish rm plan.html # or the page id; every link stops working at once- What is uploaded: the file with its runtime tag pointed at the service's copy of this CLI version, the installed packs it declares, and the images beside it, inlined by the same rules as
export. A declared pack that is not installed at that version, or an image that cannot be inlined, is an error. Each upload is limited to 1 MB after gzip. - Who can see it: pages are invite-only. Each
--invite <name>gets its own link; the token is in the URL fragment (#i=…), which browsers never send to the server, and the reviewer's notes carry that name. Send each person only their link.--unlistedopts a page into an open link where reviewers name themselves. - Notes: reviewers' notes reach
pollandfeedbacklike any other, from the reviewer's name and withremote: true. They are held for you by default: press Send (or Discard) in the review page. With--autoon the publish they go straight to the agent. The local server picks them up while apolllistens, when it starts, and when you runfeedback; offline, it keeps retrying (up to every 60 s) and local review goes on. - Replies:
reply(andpoll --agent-reply) on a published file also posts the reply to the hosted page. If the service cannot be reached,replywarns and the reply is sent when it can. - Presence: everyone on the page sees who else is there (a chip per viewer: viewing, away or writing) and the part each one last clicked in Annotate mode, outlined with their name. You are on the page too while your local review page is open: hosted viewers show as chips there, and your clicks show as an outline for them. Nothing about it is stored.
--no-presenceon a publish turns it off for that page. - Expiry: 30 days after the last publish.
- Owner token: created on the first
publish, stored in~/.austere-axi/cloud.json(mode 0600). It only controls your pages. Copying the file to another machine makes that machine a peer: it gets the same notes, and a note you send on one machine arrives sent on the other.cloud.jsonmaps pages by absolute path, so the file must sit at the same path on both machines.
The protocol is in docs/hosted-share-links.md.
Posts an austere review page on every pull request: runs austere-axi check on the pages you commit, exports each with austere-axi export, uploads them as a workflow artifact, and posts or updates one PR comment with a summary and a download link.
# .github/workflows/austere.yml
name: austere review
on: pull_request
permissions:
contents: read
pull-requests: write # to post the comment
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: MandalorianBatman/austere-axi/[email protected]
with:
pages: reviews/*.html| Input | Default | |
|---|---|---|
pages |
required | paths or globs, one per line, relative to the workspace |
version |
latest |
austere-axi version from npm; pin it for reproducible runs |
artifact-name |
austere-review |
workflow artifact that holds the exported pages |
retention-days |
repository default | artifact retention |
comment |
true |
false leaves only the artifact and the job summary |
comment-tag |
default |
names the sticky comment; use one per use of the action in the same PR |
github-token |
github.token |
needs pull-requests: write to comment |
Outputs: ok (true when every page passed) and artifact-url.
- A failing check fails the job. The log and the job summary show each error with its statement and line. Pages that passed are still exported.
- Fork PRs get a read-only token, so the comment is skipped with a warning. The job summary and the artifact still carry the report.
- Sticky comment: the action updates only a marked comment posted by its own token's account (a bot account for
github.token). If someone else posts a comment with the same marker, the action leaves it alone and posts its own. - Triggers: use
pull_request. Do not usepull_request_targetto run pages from a fork. - Artifact: GitHub zips it and needs a login to download. Unzip and open the HTML in a browser; it works offline. No Pages preview.
- The action lives in this repo under
action/, so one ref pins both the action and the CLI contract it calls.versionstill picks the CLI.
| Variable | Default | |
|---|---|---|
AUSTERE_AXI_PORT |
4388 |
server port |
AUSTERE_AXI_STATE_DIR |
~/.austere-axi |
sessions, owner key, server log (mode 0700 on macOS and Linux) |
AUSTERE_AXI_NO_OPEN |
1 = never open a browser |
|
AUSTERE_AXI_HOST |
extra bind address (never 0.0.0.0) |
|
AUSTERE_AXI_NO_TAILSCALE |
1 = loopback only |
|
AUSTERE_AXI_LINK_HOST |
host in the printed remote link (http://<host>:<port>) |
|
AUSTERE_AXI_LINK_URL |
full origin of the printed remote link, e.g. https://austere.example.com behind a TLS proxy; the host is accepted without ALLOWED_HOSTS |
|
AUSTERE_AXI_ALLOWED_HOSTS |
extra Host headers to accept (reverse proxy) |
|
AUSTERE_AXI_DISCONNECT_GRACE_MS |
300000 |
how long a dropped connection can stay away before poll returns; 0 disables |
AUSTERE_AXI_IDLE_TIMEOUT_MS |
1800000 |
idle shutdown; 0 disables |
AUSTERE_AXI_PERSIST |
1: never idle out, as server --persist |
|
AUSTERE_AXI_NPM_REGISTRY |
npm_config_registry, else npmjs |
where components add gets packs and components search finds them, except scopes mapped with registry add |
AUSTERE_AXI_CLOUD_URL |
https://austere.kasat.xyz |
the service publish talks to (self-hosting, tests) |
AUSTERE_AXI_SYNC_RETRY_MS |
1000 |
first wait before the local server reconnects to a hosted page; it doubles up to 60 s |
CHROME_PATH |
an installed Chrome, Chromium or Edge | the browser check --render uses |
The server binds to loopback, plus the Tailscale IP when Tailscale is up. Never to all interfaces.
Loopback is not the owner. Other OS users on the machine can reach loopback too. The owner key (owner.key) is what proves ownership.
- API:
/api/*answers only loopback callers that send the owner key, and refuses browser requests./api/healthneeds no key. It proves the server holds the key with an HMAC of its port and the CLI's nonce, so the CLI never talks to, or hands the key to, another program on its port. - Browser sign-in: when the CLI opens your browser, it gets a single-use login code from the server and opens the page with it. The server swaps the code for the owner cookie and removes it from the address bar. The key never appears on a command line or in output. A code works once, for 60 seconds. Another local user who reads it from the process list in that window can use it first; your browser then shows "used already", and
austere-axi logingives you a new one. - Other browsers: a printed
urlworks in a browser that already has the cookie. For any other browser (SSH with a forwarded port,--no-open, a second browser), runaustere-axi login [file]and open the link it prints. - Owner cookie is an HMAC of the key, not the key. Browsers share
127.0.0.1cookies across ports, so a program on another local port that your browser visits can read it. With it, that program can open your pages and the hub as you in a browser, but cannot call/api/*, which needs the key.
Files. On macOS and Linux the state dir is set to mode 0700 on every run. New files in it (owner.key, server.log, and hub.db, which holds every note) are mode 0600. Files an older version created keep their old mode, behind the 0700 dir. On Windows, file modes do not apply; only your user profile's permissions protect the state dir.
Requests.
Hostheaders must be on an allowlist (DNS-rebinding defense).- Browser routes that change state refuse a foreign
Origin. - Invite tokens are random, compared in constant time, and kept out of the log. A reviewer link can only add notes to its own page. See Team review.
- The server serves files only from the page's own directory, with no dotfiles.
Page sandbox. The page runs in a sandboxed iframe with no same-origin access; packs run there too. Page files are also served with Content-Security-Policy: sandbox, so a page opened in a popup or its own tab stays sandboxed. In review, the review page opens new-window links only if they are external http(s): or mailto: links. A popup the frame opens any other way stays sandboxed.
Packs. Third-party packs need --trust unless your trust list names them. Installed packs must match their lockfile hash; CDN loads use Subresource Integrity.
Telemetry. OpenUI's Node side has PostHog telemetry; the CLI sets OPENUI_TELEMETRY_DISABLED=1. The browser runtime sends none.
- Image attachments in feedback.
- Live data:
QueryandMutationare off, so pages are static. - Themes: one dark theme only.
npm install
npm run build # dist/: runtime, packs, pack kit, spec, catalog, review UI, cli.mjs
npm test
npm run typecheck # tsc over src/cli, src/shared and the pack types
node dist/cli.mjs examples/session-migration.htmlRepo layout, website, playground, benchmark, CI and release: CONTRIBUTING.md.
Bug reports, examples, docs fixes and packs are welcome; CONTRIBUTING.md has setup and the rules. Issues labelled good first issue are small and have a clear finish line. Security problems: report privately, not in an issue.
MIT. Builds on OpenUI (MIT). The review loop design comes from lavish-axi (MIT).
