Skip to content

Releases: 2nd1st/open-mcp-apps

v0.6.0 — the engine becomes a runtime

Choose a tag to compare

@2nd1st 2nd1st released this 02 Sep 04:48

The engine becomes a runtime, not just a container. One release, built over two weeks
(plan: engine repo docs/runtime-plan-2026-08-16.md, internal): apps written outside a chat
and installed from a file, functions that run on their own thread and reach the network, widgets
that declare what they connect to, and the author-side kit those three make possible.
oma.contract is 3 — see Fixed for the one change an existing app can notice.

Changed

  • An app document has no size cap. The 200,000-unit write-side ceiling (MAX_APP_HTML and
    the ui_too_large refusal behind it) is gone — from the store, from install-app.mjs, and
    from the package barrel. The only size that ever did load-bearing work is on the READ side,
    where get_app returns the source in windows, so a large app now costs windows rather than a
    refusal. empty_ui stays: it is a validity rule (an app is something a person opens), never a
    size floor. The /rpc and /mcp request-body limit moves 2 MB → 64 MB and is named
    MAX_BODY_BYTES, because it is transport self-defence and was never a statement about how big
    an app may be.
  • App functions run on a worker thread, not a synchronous vm closure. A body may
    await; the store stays synchronous to it (api.list(...) returns rows) over a blocking
    cross-thread call. The time budget is enforced by worker.terminate(), which ends running code,
    pending timers and in-flight sockets — a harder cancel than vm's timeout, which could only
    interrupt code it was running. Every function written for the old executor runs unchanged; the
    async_not_supported error is gone (a returned promise is awaited). Default per-call wall clock
    2 s → 10 s, sized for one HTTPS round trip; the other budgets (100 writes, 200 reads, 32 KB
    result) are unchanged. Up to 8 calls run concurrently; further calls queue without burning their
    own deadline. src/functions.mjs's header now says why the old "synchronous is the contract" argument
    was right then and what replaces it — including the one property that was given up (no work
    after the window) and why the receipts already covered it.
  • A per-app ui:// resource's _meta.ui.csp (and the openai/widgetCSP twin) is computed
    per read from the app's declaration ∪ the user's additions, instead of a constant empty
    allowlist. The engine adjudicates nothing: it merges and relays; hosts enforce. An app that
    declares nothing produces byte-identical metadata to 0.5.9. The engine's own two policies — the
    runner child's CSP meta and the /view response header — are built from that same merged
    declaration, so an app that works in a host works in the browser viewer; both floors are
    unchanged byte for byte. app_html returns the merged csp, which is how the loader gives a
    sandboxed child its policy.
  • An app whose ui carries asset references is read-only to the model: get_app returns
    the template, while edit_app, the model's save_app, restore_app and promote_app refuse
    with built_outside ("source lives outside this store; rebuild and re-install with
    install-app.mjs"). Rebuilding and re-pushing is the edit. Detection is structural — what the
    stored document is, never an author string. Human-pushed apps keep no version stock: after a
    human push onto a human-authored app, revisions older than 7 days (HUMAN_HISTORY_KEEP_DAYS)
    are dropped; the current revision is never swept, and AI-authored apps keep everything.
    save_app refuses bad_asset_ref for a reference the file plane could never store; existence
    is deliberately not checked at save (a push is two writes and either order must work) — a
    missing asset is reported loudly, in the widget, at serve time.
  • The authoring guide's "keep it under ~100KB" is advice again, and now says what it was
    actually about: do not write a complex app in one shot — save a skeleton, then grow it with
    edit_app; data belongs in the collection; source is read in windows.

Added

  • fetch in function bodies, with AbortController/AbortSignal, URL,
    URLSearchParams, setTimeout/clearTimeout, TextEncoder/TextDecoder, atob/btoa.
    Egress is not filtered: the engine runs on the user's own machine over the user's own network.
    The worker has an empty env and its own 256 MB heap, and its stdout never reaches the parent's
    (which, on the stdio transport, is the protocol channel).
  • manifest.functions[name].timeout_ms — a per-function deadline, declared where the signature
    is. The engine sets no policy ceiling on it: the real limit is the host's own tool-call
    timeout, since call_function is an MCP tool call the host waits on — past it the body only spins
    against a result nobody reads. The default, when nothing is declared, is 10 s. The save door keeps
    only a sanity floor (a positive integer the timer can hold), not a cap on how long a function may
    run. The deadline covers the whole call — thread start, body, and every await inside it — and is
    enforced by worker.terminate(), the cancel that keeps a runaway loop or a hung fetch from pinning
    a worker slot. (A SaaS sandbox that wants a cap sets its own; the OSS engine does not.)
  • Secrets are reserved, not delivered: api.secret exists and refuses, and settings keys under
    secret: are refused by the generic data_* writers and by security_set alike — the namespace
    is held empty for the release that fills it (entry will be the viewer's settings UI, never a
    model-facing tool).
  • Apps can declare where they reach. A manifest may carry csp with the four keys from the
    MCP Apps spec — connectDomains, resourceDomains, frameDomains, baseUriDomains
    validated for shape at the save door (RUNTIME.md §5.1). Users can add origins of their own, per
    app or globally, through the reserved settings keys policy:csp:<app> and policy:csp:*
    (written with security_set, which now refuses a value that is not a well-shaped JSON object of
    origins). open_app renders through the universal loader — one resource serving every app — so
    that resource's _meta.ui.csp now carries the union of everything declared in the store
    (every app's manifest.csp ∪ the user's additions), computed per read: hosts are asked to allow
    the union, and the runner child inside the loader is narrowed back to its own app. A store with
    no declarations serves the 0.5.9 bytes exactly. The per-app resource (OMA_DYNAMIC_TOOLS=1)
    still carries only its own app's declaration, and the engine's own viewer and runner always
    build from the same merge. The loader's public cache hint is gone with this: its answer is
    store-derived, so it is not the same for everybody. Whether a host reads the list-time or the
    read-time _meta is a host-matrix question, not settled here.
  • Apps built outside the chat. An app produced by a build pipeline now installs as a readable
    template plus its bundle: the template references its own build output
    (<script src="oma-asset:app.js">, <link rel="stylesheet" href="oma-asset:app.css">) and the
    files live in that app's file plane. The engine inlines every reference at serve time — the
    widget CSP allows no external subresource, and a host iframe could not reach this machine
    anyway. Documents with no references are served byte-identically to before.
    install-app.mjs --manifest <manifest.json> (the declaration as its own file, the shape a build
    emits) and repeatable --asset <path> (build output into the app's file plane, keyed by the
    file's basename); --update is unchanged. types/window-oma.d.ts types the API an app sees
    (index.d.ts types the engine's Node API, which an app never touches), pinned against the same
    name list RUNTIME.md and test/runtime-contract.mjs share. RUNTIME.md §6.1 is the target a
    build step has to hit — including the one thing bundlers get wrong: <script type="text/oma-function"> blocks must be emitted into the template, never bundled.
  • A host-CSP probe app under test/probes/host-csp-probe/: install it, open it in a host, and
    it writes what that host does with an app's csp declaration into its own collection — nine
    cells (declared connect, loopback connect, resource, frame, function fetch, a two-step
    list-vs-read _meta test, an undeclared-origin control, blob Worker, WebAssembly), each with
    the policy the browser actually applied, captured from securitypolicyviolation. Two of the
    cells measure something no app can declare — McpUiResourceCsp is four lists of domains, so
    there is no way to ask for a Worker or for WebAssembly, and this engine's own floor grants
    neither (open-decisions D-20). The README is the run-book, one row per host and per door
    (open_app vs open_<name>).
  • types/oma-function.d.ts — the args/api a function body sees; and both type files now
    actually ship: they are in the published snapshot and declared in the package exports, so
    /// <reference types="@2nd1st/open-mcp-apps/types/window-oma" /> resolves under bundler
    and node16 (it was TS2688 under both — the file existed in the repo and never reached
    npm). oma.callFunction<T>() is generic over what the body returns.
  • install-app.mjs --prune-assets — opt-in removal of files this push neither carried nor
    referenced, for builds with content-hashed output names (without it every rebuild leaves the
    previous bundle behind in the app's file plane).
  • list_apps rows carry a functions count when an app's manifest declares any:
    · N function(s) in the text line, functions: n in the structured row, and absent — not
    0 — when there are none. Names and signatures stay one get_app {slot:"manifest"} away.
    No outputSchema was added, so tools/list is byte-identical.

Fixed

  • call_function now says what the function returned on the text channel too. The reply
    carried the...
Read more

v0.5.9 — what initialize declares, the engine now does

Choose a tag to compare

@2nd1st 2nd1st released this 16 Aug 06:16

What initialize declares, the engine now does.

The handshake every MCP host reads first is a set of promises about verbs, and two of them had never been kept — for as long as they existed, and without a single test going red, because nothing in the repository ever called the verbs the promises were about.

  • resources: { subscribe: true } was declared from the first release. A host that took it at its word and sent resources/subscribe got -32601 Method not found — on every legacy protocol version the SDK negotiates, which is the wire every shipping host speaks today. The engine already pushes notifications/resources/updated to everyone on the connection for every app-plane write (a larger promise than "subscribe"); what was missing was a handler that accepts the request. Both resources/subscribe and resources/unsubscribe now answer. The bit stays declared, because on the 2026-07-28 wire it is what makes a subscriptions/listen filter naming our URIs honourable — and that verb the SDK serves itself.
  • tools.listChanged was written as a conditional meant to say false unless the per-app openers are on. It said true in every mode: an absent key is not false to the SDK, which fills the bit in with ?? true the moment a tool is registered. Measured in all three settings. It is now written out unconditionally — true with OMA_DYNAMIC_TOOLS=1, false without.

A new suite, test/capabilities.mjs, starts the server over real stdio, reads what was declared, and then calls each declared verb — on the legacy wire and, separately, on 2026-07-28. Run against the code as it was, three assertions went red by name.

Added

  • A bug-report form on the public repository whose first four fields are the tuple nearly every defect in this project's history has turned out to be a property of: host × host version × channel × surface. One template, blank issues still enabled, three contact links (KNOWN-ISSUES.md first, Discussions for questions and app wishes, the private advisory channel SECURITY.md names). No "app request" template on purpose — this project's answer to I want an app is to have your AI build it.
  • One sentence when a person runs the server by hand. Pasting npx -y @2nd1st/open-mcp-apps into a terminal used to end in a cursor that stopped: zero bytes on stdout, because a stdio MCP server prints nothing until a host speaks. Now, when — and only when — stdin is a terminal, one line on stderr says what this process is and where the command belongs (your host's MCP config). Hosts spawning over pipes never see it. Both READMEs' fact table renamed the row that invited the paste from Run it to Command.
  • A net under every tool call in the smoke suite, reading each result the way the model reads it, so undefined, NaN and [object Object] cannot reach the model as prose again. 0.5.7 fixed one such row; this pins the species. A row deliberately broken elsewhere was caught by name; the tree as shipped scans clean.

Fixed

  • A comment at the top of src/server.mjs said the repository must not advertise npx, and the README's first install path is npx. The line predates the scoped package. The rule is scoped only — bare npx open-mcp-apps still runs a stranger's package — and the comment now says exactly that.

The tool surface did not move — 44,911 B, byte-identical to 0.5.8 — because none of this rides tools/list.

Verified before this note was written: the public CI ran on the snapshot commit itself (6f6d2fd) — test (22) and test (24) both green, no MCP host present: https://github.com/2nd1st/open-mcp-apps/actions/runs/31930937332

Full detail: CHANGELOG.md

v0.5.8 — four green lights that could not have gone red

Choose a tag to compare

@2nd1st 2nd1st released this 15 Aug 21:55

Four green lights that could not have gone red.

The largest group below shares a shape rather than a subject: a reading was trusted for an answer it was structurally incapable of giving.

  • install.mjs --check called a Codex entry clean by parsing a line on which every value is printed *****.
  • The MCP Registry badge read servers[0] off an endpoint that returns every version ever published under a name, oldest first — so it printed a version that got staler with each release.
  • The link-closure gate this project added one release ago printed CLEAN over the densest batch of relative links in the repository, because one regex read left to right can only ever see the inner half of a nested badge.
  • openai/codex#28912 closing as completed was quoted in the place a reader looks to find out whether a failure still bites — the closure of an umbrella, which cannot speak for any single failure filed beneath it.

None of the four was a wrong number waiting to be corrected. Each was an instrument reporting on itself while wearing the clothes of a report about the world.

Added

  • The engine's first MCP prompt, get_started. The README used to open by asking new users to type a sentence; that sentence is now an entry in the host's own menu. Its body is an instruction to the AI — look at what you already have, check the App Store, read get_app_guide before writing anything — not a copy of the authoring guide, and a test fails if any line of it starts appearing verbatim in the guide.

Removed

  • The OMA_DYNAMIC_TOOLS=1 the installer wrote into host entries since 2026-07-28. It existed because a chat-surface regression swallowed the loader widget's boot call and open_app hung at "Loading app…"; it cost one approval prompt per app. Re-measured 2026-08-16 on Claude Desktop 1.30096.5: a registration without the flag renders through the universal open_app, clicks write, and the data survives a restart. Re-running the installer removes the key from your host entries — and only that key.

Fixed

  • --check reported a Codex entry as clean while it carried the retired key, because it parsed output where every env value is masked. It now reads the value where the value exists, and reports stale when it cannot read it at all rather than assuming the best. The same commit closes a defect of the same family: re-registering a stale Codex entry used to send an add with no env, which silently dropped a user's other variables.
  • Every visitor to the npm package page was served the Chinese README. npm force-packs every README* in the package root and picked the second one. The fix is not to guess npm's rule but to leave exactly one README* at the root; the Chinese one now lives in i18n/.
  • KNOWN-ISSUES.md pointed at a closed umbrella issue in the place a reader looks to learn whether a failure still bites. It now names the open bug, and says what the closed one was.
  • The Host support table gave one row to a product with two surfaces. Claude Code in a terminal has no widget surface; Claude Code inside the Claude app does, and renders through the universal open_app (measured 2026-08-16). The section on the screen beside a terminal now leads with its cheapest form — a browser pane in the same tiled workspace, same machine, no tunnel.
  • Six passages a stranger trips over that a reader inside this repository completes from memory, plus three comments in shipped files that had stopped being true — including src/http.mjs's claim that binding to loopback "costs nothing", which stopped being true the day @live shipped: /mcp pays nothing for it, /view pays a bridge you stand up yourself.

Verified before publishing: the snapshot's own bytes ran through the public CI steps in Linux containers on node:22 and node:241616 assertions, zero failures, no MCP host present. The published v0.5.7 was run through the same container for comparison (1567), so the delta is a reading, not an inference.

Full detail: CHANGELOG.md

v0.5.7 — what a stranger receives

Choose a tag to compare

@2nd1st 2nd1st released this 15 Aug 17:22

Three defects that were only ever visible from outside, plus the documentation and the gates that let them live here unnoticed.

All three were found by looking at what a stranger receives rather than at what this repository contains: the text an MCP client is actually handed by list_apps, a link a reader on GitHub actually clicks, and the security alerts the public repository page actually shows. None of them had ever failed a test. The runtime's only behavioural change is one line of listing text — the tool surface is otherwise byte-identical to 0.5.6.

Fixed

  • list_apps told the model every app was undefined characters long. The rendered row read a field named html_size; the query behind it has produced length(ui) AS ui_size since the manifest split, and a missing property in JavaScript is undefined, not an error. Every row of every listing, in every host, printed (undefined chars, by …) — while the structured half stayed correct, because it spreads the store row rather than naming fields. Found by driving the published npm package the way a client would (npx -y @2nd1st/open-mcp-apps, initialize → save_applist_apps), which is why the test now asserts on the rendered text.
  • A link in the changelog 404'd for every reader (CLA.md, deleted in 0.5.4 with the MIT relicense). It now points at the last release that carried the file.
  • TRADEMARKS.md still described the licence in the present tense as AGPL for the engine and MIT for components/. The whole repository has been MIT since 0.5.4. That file ships in the npm tarball as well as the repository.
  • Nine Dependabot alerts, not one of them on a dependency we declarehono and @hono/node-server by two paths at once, ip-address under the sdk's express-rate-limit, fast-uri under its ajv. Every repair was a patch release already inside a ^ range we had written, so npm audit fix without --force was the entire fix: package.json unchanged, package-lock.json twelve lines, npm audit --omit=dev reports 0 afterwards.
  • Documentation that had stopped being true: an assertion count, a size comparison whose argument no longer held once measured (the gap had closed from 2.7× to 1.2×), a contributor guide describing three test suites where there are twenty-three, and two places where the Chinese and English READMEs disagreed about whether an endpoint was included.

Added

  • Terminal hosts get a section of their own. A host that renders no widgets was described here as "text fallback" — which reads as no UI. Since @live, an app opened from a terminal host appears on a screen beside it, and that screen keeps following whatever the AI opens next. The host table now carries that path, with its cost stated: the conversation still shows no widget, and the viewer binds 127.0.0.1, so a second device needs the tunnel described above it.

Changed

  • scripts/publish.mjs checks link closure over the staged snapshot (step 7b): every relative markdown link in a published .md must resolve inside the published file set. It covers the two cases nothing else could — a target deleted from the repository, and a target that exists here but is not in the ALLOWLIST, which no check running against the internal tree can detect by construction.
  • The release version now has a machine counting its homes. It lives in six places; two of them — server.json (the MCP Registry contract) and lhm.plugin.json (the LobeHub one) — were checked by nothing, and one of them silently stayed at 0.5.4 through a release, which merged a directory update into an old entry. test/invariants.mjs now pins both, along with the package identifiers they carry.
  • A byte count in RUNTIME.md is now derived from source rather than copied by hand; the previous value had been wrong by more than a factor of two.

Verified before publishing: the snapshot's own bytes were run through the public CI steps in Linux containers on node:22 and node:24, on both architectures — 1567 assertions, zero failures, no MCP host present. That check exists because 0.5.5 shipped with a red CI after being "verified" on a machine that had Claude Desktop installed.

Full detail: CHANGELOG.md

v0.5.6 — the public CI goes green

Choose a tag to compare

@2nd1st 2nd1st released this 15 Aug 15:14

0.5.5 fixed two of the three things that broke the test chain on the public repository; the third was standing behind them. npm test stops at the first failure, so each repair only revealed the next — and this one was invisible to me for a second reason worth recording: I verified 0.5.5 by running the snapshot on a machine where Claude Desktop is installed. The CI runner has no MCP host at all.

  • test/install-paths.mjs could never pass on Linux. Its fixtures build a fake HOME and write Claude Desktop's config into Library/Application Support/Claude — the macOS shape. On Linux the installer looks under ~/.config, found no host, exited 1, and took every suite behind it down. The fixture now resolves that path per platform, the way install.mjs does.
  • The same fixtures could write into the person running them. A fake HOME is a complete boundary only on macOS; elsewhere the installer honours XDG_CONFIG_HOME and APPDATA, which on a developer machine point at the real user. The sandbox now names those too.

Measured both ways: 30/30 on macOS, 23 suites green in a Linux container.

v0.5.5 — the copy other people get

Choose a tag to compare

@2nd1st 2nd1st released this 15 Aug 15:05

Nothing in the runtime changed (the tool surface is byte-identical to 0.5.4). Everything here is about the copy of this repository other people get.

Fixed — three things that only ever failed somewhere else

  • pnpm install had never worked on any version: the licence collector in prepare assembled node_modules/<name> by hand, which is npm's layout — under pnpm the first segment is .pnpm, the content-addressed store, and reading its package.json is an ENOENT that fails the whole install. Package roots now come from the bundler's own input paths. (This is also why the Glama build of this repo had never gone green.)
  • The public repository's CI had never been green: npm test is an && chain with a docs-reading suite second, and docs/ never enters a public snapshot — so the other 21 suites had never run here at all. Verified against the real snapshot: npm ci, node build.mjs, npm test all exit 0.
  • require("@2nd1st/open-mcp-apps/package.json") threw ERR_PACKAGE_PATH_NOT_EXPORTED.

Added — a Dockerfile that actually runs the server (with the glibc/prebuilt trap that makes a green build die on first query written into the file), .mcp.json, and the registry contracts that had been living in a scratch directory.

Changed — both READMEs rebuilt for the reader they actually have: a fact table and three complete host-config blocks first, deep material below. Two quoted assertion counts turned out to be wrong.

Full detail: CHANGELOG.md

v0.5.4 — MIT

Choose a tag to compare

@2nd1st 2nd1st released this 15 Aug 13:56

The engine is now MIT licensed — one license for the whole repository (components/ already were). The trademark reservations in TRADEMARKS.md are unchanged. The CLA is retired: MIT in, MIT out, no signature ceremony.

Why: the MCP ecosystem is MIT end to end (the official SDKs, the hosts we bridge into), embedding the engine inside other hosts is exactly how it grows, and several registries reject copyleft outright. Nothing tightens for anyone; everything loosens.

Also in this release: version numbers 0.5.2/0.5.3 were burned by an npm publish incident (npm-side only — GitHub v0.5.2 exists and is fine); the npm package resumes at 0.5.4 as @2nd1st/open-mcp-apps.

v0.5.2 — npm packaging

Choose a tag to compare

@2nd1st 2nd1st released this 15 Aug 13:33

Packaging-only release: the engine is now on npm as @2nd1st/open-mcp-apps. One-line install for any MCP host: npx -y @2nd1st/open-mcp-apps. No behavior changes — tool surface byte-identical to 0.5.1.

v0.5.1 — an app stops being a guest and starts owning a screen

Choose a tag to compare

@2nd1st 2nd1st released this 15 Aug 11:34

An app stops being a guest in someone's conversation and starts owning a screen. 0.5.0 made an app's declaration a first-class object; 0.5.1 asks the next question — where is this app standing? — and gives it three answers: a card in a chat, a page it owns, or a region inside a host's panel. One contract covers all three.

Highlights

  • The stage contract. Every app used to hand-roll its own outer card, hero header, root width and overflow policy — 7 different frame styles and 10 different width values across the store, double-rounded corners on phones. Now the kit owns the wrapper: in a chat the app gets one standard conditional card (and on narrow frames, none — the host's widget frame is the card); on a page or panel it renders bare, edge to edge. Apps declare their width posture in one manifest word (stage.width: column | wide | fluid), and all 22 store apps have been rebuilt to the contract — decorative identities (gradients, rings, accent bars) moved into content, where stripping the frame can't take them.
  • @live — a brick, not a route. A display that follows whatever the AI opened last is now a primitive: any app can place oma.embed("@live"), and the engine keeps a single overwritten pointer (schema v7, deliberately not a ledger event — opening an app is a glance, not a data change) and pushes it over the existing SSE channel. The store ships a ready-made Live Display app: install it, open /view/live on a spare tablet, and the screen switches by itself as the AI works. Apps that contain @live declare stage.display and are excluded from the pointer — a wall never points at itself.
  • Panel hosts are first-class. Two opt-in URL words for anyone embedding /view in their own chrome: ?chrome=0 (no viewer bar or stage — the panel draws its own) and ?nav=intent (app→app links become an openmcp:open-app postMessage instead of navigating the frame — "open X" is a request to the host, not this page's navigation). Embedded child documents now inherit their parent's context, and stage.display apps render chrome-less on /view by default.

Fixed, found on real devices

  • No sandboxed child document had ever reported its height — a minified-bundle name mismatch landed in the broadcast's own catch, silently, since the runner shipped. Every embedded frame sat at 140px forever. One line; frames now size to content.
  • Forms inside embedded apps couldn't submit at all: the sandbox lacked allow-forms, and Chrome refuses the submit event before dispatch — so every onsubmit handler (17 of 23 store apps hang their add/edit on one) never ran. Granted, with the actual submission still triple-walled off.
  • Apps taller than their frame couldn't scroll in fixed-height contexts: seven store apps pinned overflow: hidden on the root, which is correct in a host that grants height and a dead scroll wheel in one that doesn't. The viewer now unlocks the root; the apps handed in their declarations.

Upgrading

  • A 0.5.0 store upgrades in place on first open (v6 → v7: one added table, one transaction). A store opened by 0.5.1 cannot be opened by 0.5.0 — upgrade every host registration together.
  • Tool surface, oma.contract, and the runtime API are byte-for-byte unchanged from 0.5.0. manifest.stage is optional and additive.

Full detail, including the measurement behind every line: CHANGELOG.md.

v0.5.0 — the declaration becomes an object, the library becomes the App Store

Choose a tag to compare

@2nd1st 2nd1st released this 14 Aug 08:22

Breaking, and the largest release since the engine existed. Three things moved at once: an app's declaration became a first-class object the engine stores; row deletion became something the engine confirms rather than something every app had to remember to ask about; and an app can now expose a function — a data→data closure the AI can call without rendering anything.

Highlights

  • The declaration is an object. save_app takes ui and manifest as separate slots, the stored manifest column is authoritative, and history snapshots both slots — restore_app and undo bring back the pair. A document that still embeds an #oma-manifest block is refused with a pointer at the parameter.
  • Deletion is confirmed by the engine, inside the store transaction every tool, batch and widget bridge passes through. Twelve apps used to implement their own arm-then-delete; the ones that forgot simply deleted. No app author writes confirmation UI anymore.
  • Functions. An app can declare data→data closures; the AI calls them through call_function without a render. Bodies are synchronous by design, the seat is opt-in at createEngine and absent by default.
  • The library is now the App Storeapp_store_list, app_store_preview, install_from_app_store, system app app-store. Old library_* names are gone, not aliased. The store grew from 17 to 21 ready-made apps, now shipped as directories (components/<name>/{ui.html, manifest.json, fixtures.json}).
  • Settings and the App Store were rebuilt — the two system apps a user actually sits in, each with a narrow and a wide form, themed entirely through host tokens.
  • promote_app turns a visual into an app in one call, and edits by range let the AI patch a large app without re-sending the document.
  • SDK v2. The engine moved to @modelcontextprotocol/{server,client,node} v2, and a line-by-line audit took the tool surface from 36 to 33. The widget runtime contract bumps to oma.contract = 2.

In a real chat, measured on real hosts

A long tail of fixes came out of live sessions on Claude, ChatGPT and Codex rather than from a test rig:

  • A widget's height is now governed by the host: the engine only unpins documents that froze their own measurement (overflow:hidden, min-height:100vh), so an app that grows is seen to grow — and a card in a conversation no longer nests a private scrollbar.
  • The App Store no longer pours its whole catalogue into a conversation: Discover is compact in chat, and the only live preview is the detail view you asked for. In the browser viewer, every preview now renders with its sample data (two independent bugs had been emptying 21 of 21).
  • A refreshed widget on ChatGPT could come back permanently empty — the host replays a different call's envelope; the widget now writes its identity down through the host's own state channel and recovers.
  • Nine store apps crashed on first open in a chat; a filtered app could not shrink back down; the store forced a horizontal scrollbar on narrow widgets — all fixed, each with the measurement that found it recorded in the CHANGELOG.

Upgrading

  • A 0.4.2 store upgrades in a single open with nothing lost (v4 → v5 → v6, one transaction each, manifests backfilled by replaying the ledger). Rehearsed against real stores before shipping.
  • A store opened by 0.5.0 cannot be opened by 0.4.x — upgrade every host registration and the browser viewer together.
  • Hosts holding a cached tool list must re-list: five tools are gone, two are new, four are renamed.

Full detail, including the reasoning and the measurements behind every line: CHANGELOG.md.