sergeysuaib-ui/manaurum-dev-sdk
Build, test, and deploy ManAurum OS Platform v2 apps with shared SDK skills and templates.
3.19.0 - the starter accepts only tokens minted for its own app, and the SDK pages describe SDK 2.5.0 as served
Summary: The starter app refuses a sign-in token that was made for a different app, and the guide describes the browser helper as ManAurum serves it now.
Why
- Core names the app in every user_context (MAN-3231, sergeysuaib-ui/manaurum#2380, live
since production's 2026-10-05 deploy at
5c2dbdb62):audis["manaurum-app", "<the app's v2_apps id>"], and the deploy injects that id asMANAURUM_APP_ID. Core's bundled verifier (_manaurum_runtime.py::verify_token) requires it, with the tenant, and refuses a person pass or a system token, and so does the scaffold CLI 0.3.3 writes (MAN-3268, Core45f13bc07, #2402). The starter still accepted any token with the shared audiencemanaurum-appand checked only theapp_idandtenant_idclaims, so a person pass or a system token for the app would have been read as a user context had it carried those claims, and nothing tied a token to the app's own id. - Production serves
manaurum-v2.mjs2.5.0 since the same deploy (checked 2026-10-05). 3.18.1 removed the pre-deploy notes from the capability pages, butsdk-api.md,SKILL.mdStep 2.5 and the starter's comments still named 2.3.0 as what production serves and had apps feature-detectonLocaleChange.
What changed
- Starter
src/auth.pybinds the user_context to its app (MAN-3268), as Core's bundled verifier does: the audience must beMANAURUM_APP_ID(the sharedmanaurum-appproves nothing), withaud,iss,expandiatrequired; a token withtyp(a person pass) orscope(a system token) is401 user_context_wrong_kind; a missing or foreign audience is401 user_context_wrong_app;MANAURUM_APP_IDunset is503 manaurum_app_id_not_injected, asMANAURUM_TENANT_IDunset already was. The slug and tenant checks, the error shapes,locale/dir,server_languageandPersonClaimsare unchanged; the person pass still requirestyp: "person"(and now saysrequire_issexplicitly), so neither kind stands in for the other. - Starter tests. The
user_contextfixture mintsaud: ["manaurum-app", APP_UUID]and the autouse fixture setsMANAURUM_APP_ID. New: another app's audience, the shared audience alone, no audience, the right audience with another slug,MANAURUM_APP_IDunset, a missingiss, a person pass and a system token refused as a user context, a token carryingscope, a system token refused as a person pass, a user context refused as a person pass with its code, and a pass withouttyp: "person"refused. Eight of them fail against 3.18.1'sauth.py. - The verification docs bind the app.
v2-platform.md's four steps require yourMANAURUM_APP_IDas the audience, notyp/scope, the tenant and the slug, and say which test covers what; its env table namesMANAURUM_APP_IDas the audience, and on abyohost you set it yourself. The scheduled-jobs example requiresaud/iss/expand checks the tenant, and says thescopecheck is what refuses a user context or a person pass. README's model,SKILL.md'sauth: "user"rule,manaurum-setup's env table, the starter's README andmain.pydocstring say the same. - SDK 2.5.0 as served.
sdk-api.mdstates 2.5.0's behaviour: a plainapp.onLocaleChange(...)call andstop()in the example, no feature detection, and a "since 2.4.0" / "since 2.5.0" or "versions before 2.5.0 have none of the three" only where it helps; the notes on what production served on 2026-10-04 go.SKILL.mdStep 2.5 says why the inline listener stays first (it runs before any module). The starter'sindex.htmlandtests/test_static.pycomments no longer speak of 2.3.0 as the SDK. - On top of 3.18.1.
publishing.mdsays the dev-apps routes are gone (production answers404there) instead of telling people not to use them. Theos.ai.speaksection adds that the app's speech setting is per workspace (so it applies only when a workspace resolves), that a voice giving way keeps an OpenAI-voiced app speaking after an admin moves it to Gemini, that the response'smodelandvoicesay what spoke, that Gemini's voices speak the text's language, thatspeech_setting_invalidis never silently replaced, the output encoding per provider, and the library voice's overhead in tokens; "Who pays" lists the per-provider order once, for transcription and speech.os.ai.providers'transcribeentry covers speech on an OpenAI model only. - The contract copy stays at Core
b9980413e, 3.18.1's sync, which is newer than production's5c2dbdb62; re-runningpy -3.12 scripts/sync_contract.py --monorepo C:/Users/sergei/Desktop/Manaurum --ref b9980413ec890ecf161d18e865ce9f38855a1e4freproduces it byte for byte. The capability reference's header and footer name it. - CLI 0.3.3 (cli-v0.3.3,
Core sergeysuaib-ui/manaurum#2402): README installs its wheel and
open-claims.txt's MAN-1385 line names it (PyPI still answers404, re-checked 2026-10-05). 0.3.3's starter binds the user_context as this starter now does, refuses anapp_idending in a newline, and checks typed event declarations.check_app.pyrefuses that newline too: it compares the slug withre.fullmatch, so the contract's$no longer matches before a final\n.
Not in this release
- Typed event declarations (MAN-3063 / MAN-3064, #2377, in the schema since 3.18.1's
sync): a
provides.events[]entry that is an object withpayload_schemamust carrynameand an object JSON Schema, and the deploy validates it. The skills do not teach it, andcheck_repo.pyasks for nothing. - Between production's
5c2dbdb62and the syncedb9980413e: Studio can pause and resume an app, with a paused page that names who paused it, and a disabled app's signed-out page, API call and asset all answer503before any login redirect (MAN-3137, #2400); a Sandbox story animation and a Finance release. Not deployed when this was written, andv2-platform.md's503 app_disabledrow stays true. - The deploy's own refusal of a slug ending in a newline (MAN-3270, Core
sergeysuaib-ui/manaurum#2402) is on Core's
mainbut was not deployed on 2026-10-05: until it is, production's deploy still lets"probe-app\n"through, though CLI 0.3.3 andcheck_app.pyrefuse it. MAN-3271 (the CLI scaffold's page settingdirfor a Hebrew browser outside the shell) changes nothing here: this starter's page has done that since 3.16.0.
Checked
py -3.12 scripts/check_repo.py: clean.py -3.12 scripts/linter_mutations.py: every mutation caught.py -3.12 scripts/smoke_tools.py: passes. The starter'spytest -q(requirements.txt and requirements-dev.txt in a temporary venv) passes;check_ui.pyandcheck_app.pysaycleanfor the starter, andcheck_ui.pyfor the patterns page.- Against Core:
_manaurum_runtime.py,user_context_jwt.py,person_pass.py,system_token.py,capabilities/ai.py,services/voice.py,ai_service.py,capabilities/completion_context.pyand the guide's "Voice" section and error table at5c2dbdb62; the CLI'sauth.py.templateand its tests at45f13bc07. python-jose 3.5.0 raises a plainJWTErrorfor a missing required claim, which the audience mapping covers.
3.18.1 - os.ai.speak speaks with Gemini too
Summary: os.ai.speak takes an optional model and can speak with Gemini; the capability reference says how the model and voice are chosen.
Why
Core sergeysuaib-ui/manaurum#2385 (merged 2026-10-04) added Gemini 3.8 Flash TTS and
Flash-Lite TTS next to OpenAI. The reference still called os.ai.speak OpenAI only, with
five voices and no way to choose a model.
What changed
capabilities-reference.md,os.ai.speak:- the optional
model; - the voices of each model, with
en-us-bodias the Gemini default; - the order that picks the model (the request, then the app's voice setting in Settings, then Manaurum's default);
- that the speaking model's provider pays;
- the
400 unknown_voice,412 integration_not_configuredand412 speech_setting_invaliderrors; - Gemini's 64 kbps MP3 and its per-call overhead.
- the optional
- The 2026-10-04 Core changes are live in production (checked 2026-10-05), so the notes
saying they were not deployed are removed. That covers
os.ai.speak, the voice-key funding,os.directory.list_users, thedraft-slug refusal, the removed dev runtime, and the app id inaud. - The platform contract is re-synced from Core
main@ b9980413e (scripts/sync_contract.py). That brings inmodelonos.ai.speakand the new error codes (unknown_voice,speech_setting_invalid), together with whatever else Core merged since the last sync, including the manifest's newpeoplesection.
3.18.0 - CLI 0.3.2, and Core's 2026-10-04 merges ahead of their deploy: the SDK's language, speech, the team list, no dev runtime
Summary: The guide installs command-line tool 0.3.2 and describes what ManAurum merged on 4 October before it goes live: speaking aloud, a list of your team, and the person's language in the browser.
Why
Several things changed on the platform side, and the SDK either still described each one as it was before or did not mention it:
- CLI 0.3.1 is published (cli-v0.3.1).
0.3.0 refused
auth: "optional"andauth: "people"routes inapp validateand in the deploy preflight, sov2-platform.mdtold people to deploy anoptionalroute with--skip-preflight. 0.3.1 accepts both. It also refuses anapp_idthe deploy would refuse:app initbefore it writes anything,app validateand the preflight before the build, instead of a422after the upload (app_id_error,manaurum_cli/manifest.py). The README still installed the 0.3.0 wheel. CLI 0.3.2 followed the same day (cli-v0.3.2, Core sergeysuaib-ui/manaurum#2393): it addsos.ai.speakandos.directory.list_usersto the capabilitiesapp validateand the preflight check, and nothing else. manaurum-v2.mjs2.5.0 (Core sergeysuaib-ui/manaurum#2379, merged 2026-10-04) hands the app the person's language:app.locale/app.dirand the same fields on theonReadycontext, frommanaurum:init, andapp.onLocaleChange(cb)formanaurum:locale-change. Everyon…registration,onReadyincluded, now returns a function that unregisters the callback. The same PR corrected the SDK's ownfetch()docblock, which said an absolute URL is subject toegress_allowed_hosts: it is a plain browser request Core never sees.sdk-api.mdstill said the SDK drops the language.manaurum-v2.mjs2.4.0 (Core sergeysuaib-ui/manaurum#2318, MAN-2561, merged 2026-10-04) checks who sendsmanaurum:init: onlywindow.parenton a shell origin, then that pinned window alone, and it exportstrustedShellOrigins. The pages said the SDK does not check the sender and exports two names, as if 2.3.0 were the SDK.- The dev runtime is deleted (Core sergeysuaib-ui/manaurum#2296, MAN-1423):
/api/dev/v2/dev-apps, the Monaco dev mode, the capability gateway's dev-mode allow-list and thecapability_denied_in_dev_modeerror. A v2 app has two runtime modes,hostedandbyo;devstays in the schema enum but is retired and not a separate runtime any more. Four pages still described the mode, one of them as a second publish endpoint. os.ai.speak(Core sergeysuaib-ui/manaurum#2382, MAN-2727 / MAN-3256, merged 2026-10-04): text or Markdown in, the whole MP3 back as base64, five voices, at most 4,000 characters spoken per call. The same PR changed who pays foros.ai.transcribe: it is no longer the tenant's own key only, but the tenant's OpenAI integration, else Manaurum's metered voice key inside the shared AI spending limits (429 ai_spend_cap), and both voice capabilities honour AI Off. The reference called transcription BYOK and said it ignoresX-Manaurum-Workspace-Id.os.directory.list_users(Core sergeysuaib-ui/manaurum#2112, MAN-2519):{}in, the people on the app's team out, for assignee and recipient pickers. Until now an app knew that someone acted (a user id) and never who.
What changed
- CLI. README installs the
cli-v0.3.2wheel and says to use 0.3.1 or later, because 0.3.0 refusesoptionalandpeopleroutes; its paragraph onmanaurum app initno longer reads as if a word were missing.v2-platform.mddrops the--skip-preflightworkaround and states 0.3.1 as the minimum.scripts/open-claims.txt: the MAN-3235 line goes (no page says the published CLI refusesoptionalany more), and the MAN-1385 line names the cli-v0.3.2 wheel, re-checked 2026-10-04 (PyPI still answers404formanaurum-cli).check_app.py's docstring says cli-v0.3.1 has theCONCURRENTLYrule. - The
app_idrule, whereverapp_idis described (v2-platform.md's field table,SKILL.mdStep 1,manaurum-setup, themanaurum-deployrefusal table): 3–40 characters of lowercase letters, digits and hyphens, starting with a letter and ending with a letter or digit; not shaped like a UUID; not a reserved platform name; not under thedraft-prefix, which is Aurum Studio's private drafts. The deploy refuses the prefix first, with422 slug_reserved(owner_deploy_slug,backend/app/services/v2_apps/owner_deploy.py), and the refusal table gains that row. check_app.pyrefuses anapp_idunderdraft-, as the deploy and CLI 0.3.1 do; until now it saidcleanfordraft-notes. Itsapp_id_invalidmessage also names the last-character rule. The prefix rule needs no platform list, so it runs even with no contract copy beside the script, asload_contract's docstring promises such rules do.linter_mutations.pyhas one red mutation for it (draft-notes) and one that must stay green (my-draft-notes: the prefix, not the word).- SDK 2.5.0 in
sdk-api.md. The version and thesdk_versionthe SDK sends withmanaurum:ready;app.locale/app.dirin the getters table and in the context's field list; anapp.onLocaleChangerow; the unregister function for everyon…; a "With the SDK" paragraph in "The person's language" (nulluntil the handshake, outside the shell or from an older shell, fall back tonavigator.language; the callback can repeat the current value; the shell's language is not always the token's; test foronLocaleChangebefore calling it, because an older copy has none). The inline listener stays the first place the language is applied: it runs before any module loads, and it is whatcheck_ui.pychecks. The "No language" bullet goes. Everything 2.5.0 adds is marked as such: the getters and the context'slocale/dirareundefinedbefore it, the unregister function is missing before it (2.4.0 returns nothing), thesdk_versionis the SDK's own ('2.3.0'from production that day), and the page's example tests bothonLocaleChangeand what it returned before calling them. - Browser requests are not egress.
app.fetchwith an absolute URL is a plain browser request to that host: it never passes through Core, nothing filters it, and onlyos.http.fetch, called from the app's server, enforcesegress_allowed_hosts; passcredentials: 'omit'for a third-party host.sdk-api.mdsays so, andv2-platform.md'segress_allowed_hostssection adds a browser request to another host to what the list does not cover. The starter's page calls only its own relative/api/*paths, so nothing there contradicts it. - No dev runtime.
v2-platform.md§2 names two modes and says the schema'sdevis retired and not a separate runtime any more (the deploy does not look at it); itsdevsubsection is deleted. The grant check is skipped only for an active BYO host, and even then not foros.ai.complete/os.ai.providers(v2-platform.md, fromcapability_gateway.py;capabilities-reference.md). Thecapability_denied_in_dev_moderow leaves the gate table and the AI family's "none works for a dev app" bullet goes.publishing.mdloses the dev-mode publish endpoint, its poll route and its two preconditions, describes one endpoint, and says not to use the retired dev-apps route.SKILL.mdandmanaurum-setupsaydevis retired; do not use it. - SDK 2.4.0's sender check (Core sergeysuaib-ui/manaurum#2318, MAN-2561, merged
2026-10-04). The SDK takes
manaurum:initonly fromwindow.parenton a shell origin, pins that window and origin, stops forgedmanaurum:*messages before the app's handlers, and sends Drive picks only to the pinned shell.sdk-api.md(handshake, cross-origin rules,pickFromDrive) andSKILL.mdStep 2.5 say which versions check the sender and which do not; "exports two names" becomes three, withtrustedShellOrigins. The inline listener stays required: it runs before any module, and production still serves 2.3.0. os.ai.speakhas its own section incapabilities-reference.md: input (text1–20,000 characters of plain text or Markdown,voiceone of five,lang), output (audio_base64,mime_type: "audio/mpeg",voice,model,truncated), what is spoken (code as a placeholder, links as their label, at most 4,000 characters cut at a sentence end), how to play and keep the MP3, and400 nothing_to_speak. It joins the family table, the AI family ("all eight share"), theSKILL.mdcapability table andmanaurum-setup's note on voice apps.- Voice funding.
os.ai.transcribeis no longer "BYOK": its section says who pays for both voice capabilities (the tenant's OpenAI integration, else Manaurum's metered voice key, else412 integration_not_configured), which workspace a call resolves and that it needs none, the four workspace answers that refuse a call even when the tenant has its own key (400 workspace_context_requiredfor a blank header,403 workspace_context_mismatch,412 workspace_context_unavailablewhen only the Sandbox qualifies,403 workspace_context_unavailablewhen the forwarded user context names a workspace with no install the person can reach — read fromcompletion_context.py, which is more exact than Core's guide), that AI Off is403 ai_disabled, that Manaurum's key serves three models only (400 model_not_available), and that every upstream failure is502on either key, never504. The gates table, the call contract'sX-Manaurum-Workspace-Idline, the quotas table (429 ai_spend_cap),os.ai.providers(atranscribeentry pays for speech too, and its absence does not mean voice is unavailable) and theSKILL.mdtable follow. os.directory.list_usershas a section like the locations one: exactly{}in;{users: [{user_id, display_name, email, avatar_url?}]}out, ordered by email, active non-anonymous members once each; the whole team tenant, or in the public tenant only the workspace the forwarded user context was minted for (else the person's primary one) and only when one of the app's owners works in it (an app-only call there gets{users: []}); wheredisplay_namecomes from; a profile upload'savatar_urlprefixed with the OS origin; no paging; the tenant only from the verified gateway context; sensitive, with how each kind of install gets it. It joins the family table, the sensitive list, theSKILL.mdcapability table andv2-platform.md's "Names and roles", and the reference's "noos.workspace.members" line points to it.- The contract copy is re-synced at Core
7c1f09566(2026-10-04:mainwith sergeysuaib-ui/manaurum#2112, #2302 and #2380 merged), withpy -3.12 scripts/sync_contract.py --monorepo ../Manaurum --ref 7c1f095666a8e74d5904d42b8bdbfe7f4c1693bd. The manifest schema is unchanged. The contract gainsos.ai.speakandos.directory.list_userswith their inputs (34 capabilities), andcheck_repo.pyfailed until both were documented and the reference's count said 34. The strings losecapability_denied_in_dev_mode,dev_apps,hosted_runtime_not_readyand the rest of the dev runtime's words, none of which a page quotes;platform_v2_dev_modestays, as the name of an unrelated App Store flag. README's description of the copy no longer quotes a capability count, andlinter_mutations.py's capability-count mutation reads the reference's current number instead of expecting 32, which this sync turned into a missing anchor. - Dated production notes. Every page that states one of Core's 2026-10-04 merges as
current says, once per section, that it merged that day, that production had not
deployed it, and what happens until it does:
capabilities-reference.md(a note under the header — 34 registered onmain, 32 on production that day — that the speak, directory and transcribe sections point to),SKILL.md's capability rows,manaurum-setup(voice apps, thedraft-rule),v2-platform.md(theapp_idrow,dev, "Names and roles", and a note that #2380's second, per-app audience waits for the deploy),publishing.md(the retired dev-apps route still answers on production) and themanaurum-deployrefusal table (422 slug_reserved). The reference's header and footer now agree on what was read by hand at which commit and whatcheck_repo.pycompares, the footer no longer says nothing is compared automatically, and the header note says CLI 0.3.2 refuses an undeclared call to either new capability, ascheck_app.pydoes, and 0.3.1 does not know them.sdk-api.mdsays exactly which handlers 2.4.0's guard stops a forged message before (every non-capture handler, and capture-phase ones registered after it) and thatmanaurum:session-responseis left to Core's session runtime.
Not in this release
https://manaurum.com/sdk/manaurum-v2.mjsstill served 2.3.0 on 2026-10-04 after #2318 and #2379 merged. The pages describe 2.4.0 and 2.5.0 as such, name 2.3.0 as what production served that day, and say to test foronLocaleChangebefore calling it.- Production had not deployed Core's 2026-10-04 merges that day: a request to
/api/dev/v2/dev-appsstill answered401(the route was mounted), and a route added at 07:25 that morning answered404. So production predatesos.ai.speakand the voice funding (#2382),os.directory.list_users(#2112), the dev runtime's removal (#2296), the deploy'sdraft-refusal (#2368), the per-app audience (#2380) and SDK 2.4.0 / 2.5.0 (#2318, #2379). Until it deploys,os.ai.speakandos.directory.list_usersanswer404 capability_not_found,os.ai.transcriberuns on the tenant's own OpenAI key only, the retired dev-apps routes still answer, and the deploy accepts adraft-slug that CLI 0.3.1 andcheck_app.pyalready refuse. The pages say so where each fact is taught. - The starter's behaviour is unchanged. What moved in it is the
app.cssversion stamp and two comments (below). Its inline listener already checks the sender and applieslocale/dir; its comments speak of 2.3.0, which is still what production serves. - What else the re-sync window holds, untaught. Between
1256064and7c1f09566Core also added personal versions of first-party apps (MAN-3098:pv-hosts and, for those only,MANAURUM_SOURCE_SCHEMA/MANAURUM_VERSION_SCHEMA), Aurum Studio's private drafts (MAN-2915 and its follow-ups), Duties (MAN-3059–3061) andagent_jobs(MAN-3062). None changes the manifest schema or adds a capability, andcheck_repo.pyasks for none of them; the skills do not teach them. - The starter's
auth.pykeeps its audience check as it is. Core now also names the app in every user_contextaud(sergeysuaib-ui/manaurum#2380, MAN-3231), but a starter that requires it 401s against a Core that does not mint it yet, so that change waits for the deploy. Only its comments moved:auth.pyandtests/test_auth.pyno longer say Core's locale reader raisesKeyErrorfor an unknown language with nodir; Core reads that as absent since sergeysuaib-ui/manaurum#2381, as the starter always did.
Checked
py -3.12 scripts/check_repo.py: clean.py -3.12 scripts/linter_mutations.py: every mutation caught.py -3.12 scripts/smoke_tools.py: passes. The starter'spytest -qpasses;check_ui.pyandcheck_app.pysaycleanfor the starter, andcheck_ui.pyfor the patterns page.- Against Core:
owner_deploy.py,reserved_slugs.pyand the CLI'smanifest.pyon the MAN-3235 branch;frontend/public/sdk/manaurum-v2.mjsanddocs/handoff/V2_DEVELOPER_GUIDE.md§8 at2587dd77c(#2379); the capability gateway,main.pyand the guide's §1 on the MAN-1423 branch (#2296);capabilities/ai.py,services/voice.py,capabilities/completion_context.py,capabilities/directory.py,capabilities/sensitivity.py,routes/capability_gateway.py(the voice workspace map and the BYO grant exception),frontend/public/sdk/manaurum-v2.mjs, the CLI'sproject_checks.py, the guide's §3 and "Voice" section andPLATFORM_V2_CONTRACTS.mdat7c1f09566; the manifest schema just before #2382 (any capability name deploys, so a manifest that declaresos.ai.speakdeploys on production and only the call fails). - Production on 2026-10-04:
https://manaurum.com/sdk/manaurum-v2.mjsserved2.3.0, and an unauthenticated request to/api/dev/v2/dev-appsanswered401, a route that exists; the404for the route added at 07:25 is the independent review's check.
3.17.0 - an app's server knows the person's language too
Summary: An app's server and the Assistant's requests now know which language the person picked in ManAurum, so emails, replies and exports can be written in it; the starter shows how.
Why
3.16.0 taught the page to follow the person's language and said, correctly at the time,
that nothing outside the window could learn it (MAN-3244). Core shipped MAN-3244 in PR
#2341 (merged 2026-10-03): every user_context it mints - the gateway's on user and
optional routes, and the Assistant's call to /agent/<name> - now carries optional
locale (en | ru | he) and dir (ltr | rtl) claims from
user_profiles.preferred_language, and the person pass carries the same pair. Both are
absent when the person made no explicit choice or Core could not read it; Core reads
only a supported language with its own direction, and never refuses a token over the
pair. Core caches the value for 60 s, so a switch reaches a server within a minute. The
SDK still said the server could not learn it, and the starter's verifier dropped the
claims.
What changed
- The copy of Core's contract is re-synced at Core
1256064(2026-10-04). The manifest schema now has thepeopleroute mode andpeopleblock of App people (MAN-3216), socheck_app.pyno longer refuses a manifest that uses them. The skills do not teach App people yet; that is its own release, and the places that list the route modes now saypeopleexists. The sync also brought platform cron (MAN-1373, merged 2026-10-03):schedulesare live, sov2-platform.mdgains "Scheduled jobs —schedules" (what arrives, verifying the system token, the at-most-once rules) and README and the field table stop saying nothing runs them. - Starter
src/auth.py.UserContextClaimsandPersonClaimsgainlocaleanddir(Nonewhen absent), read bylocale_pair()exactly as Core'sread_locale_claimsreads them (user_context_jwt.py:84,:102-113): a supported language with its own direction, else bothNone, never an error. One difference, on purpose: Core's copy raisesKeyErrorfor a signed token whose unknown locale has nodir(Core never mints one); this one returns absent. Newserver_language(request, claims)picks the language for text the server writes: the claim, then the first en / ru / he inAccept-Language, then English. - Starter
GET /api/mereturnslocale,dirandlanguage, so a standalone tab can learn the ManAurum choice; the page inside the window keeps followingmanaurum:locale-change, which is live. - Starter tests. Each language read from the token; nine bad pairs (wrong
direction, unknown or upper-case language, no
dir, a number, nulls) accepted with no language; a token without the claims still verifies; the person pass carries the pair;server_languagefalls back in order;/api/mereturns all three. - Docs.
sdk-api.md"The person's language" now says where the server gets it, when it is absent, that it can trail a switch by up to a minute, that the Assistant's calls carry noAccept-Language, and that ananonymousroute still has onlyAccept-Language.v2-platform.mdlists the optional claims in the verification steps, on the person pass and in "Names and roles".SKILL.mdStep 2.5 points to the token instead of to what the server could not learn. check_repo.pytreats "cannot learn the language" and "language ... not in the user_context" as a stale fact, with a red mutation inlinter_mutations.py; the MAN-3244 line leavesscripts/open-claims.txt.
3.16.0 - apps speak the language the person chose, and switch when it changes
Summary: Apps built with the SDK now use the language the person picked in ManAurum (English, Russian or Hebrew) and switch the moment it changes; Hebrew screens read right to left.
Why
The shell has told every window the person's language since MAN-2289: locale and dir
in manaurum:init, and manaurum:locale-change whenever the person switches. Nothing in
the SDK used it. manaurum-v2.mjs 2.3.0 drops both (MAN-3233), the starter never read
them, its interface was English-only, and its stylesheet said margin-left and
text-align: left - so an app copied from it stayed English for a Russian reader and
was laid out left to right for a Hebrew one. sdk-api.md had one sentence about it; no
check, no preview and no line of starter code did.
What changed
- Starter
index.html. The inline shell listener (sender check, handshake, appearance and device untouched) appliespayload.locale/payload.dirfrommanaurum:initandmanaurum:locale-changeto<html lang dir>andwindow.__manaurum, and firesmanaurum-locale. A standalone tab guesses fromnavigator.languages(en / ru / he, else English). Every visible string now comes from oneSTRINGS = { en, ru, he }table throught(key)anddata-i18n, with{0}slots for code names that render as<span class="mono" dir="ltr">; numbers and dates go throughIntlwith the OS's tags (en,ru-RU,he-IL); the page re-renders on a switch, and what code wrote is redrawn rather than kept, so "Saved at 14:05" follows the new language's clock. A locale the shell sends thatSTRINGShas no table for resets<html>to English, left to right, so the head'sLOCALE_DIRandSTRINGScannot drift into English laid out right to left. The note field hasdir="auto", and the back arrow.flip-rtl. - Starter
app.css. Every physical side is logical now (border-inline,text-align: start,margin-inline-end,padding-inline-start,border-inline-start),.monois bidi-isolated,.flip-rtl:dir(rtl)mirrors an arrow, and the header names this as its fourth rule. Checked in headless Chrome: the starter mirrors completely under?locale=he, and switches live. - Starter tests.
test_static.pyasserts that the listener applieslocaleanddiron init and onlocale-changebehind the sender check, the standalone fallback, that the three catalogues carry the same keys and the same{0}/{time}slots, that every key the code names exists, the fallback to English for a locale with no table, the save time formatted when drawn, theIntltags, and no physical side inapp.css. check_ui.pyfails anindex.htmlthat never writeslocale/dirfrom the payload onto<html lang dir>(the same shape as the appearance rule, so the standalone guess does not count), one with nomanaurum:locale-change, and every physical side in a stylesheet -margin-*,padding-*,border-left/right, a corner likeborder-top-left-radius,left/rightpositioning, a four-value shorthand whose left and right differ (padding: 4px 0 4px 20px),text-align/float/clearleft or right - naming the logical replacement. What mirrors anyway is not flagged: both sides alike in one rule,margin: 0 auto,left: 50%. A write in a helper counts when the helper is called with the payload's values (setLanguage(p.locale, p.dir)), and an app in one language on purpose declares it,<html lang="he" dir="rtl" data-languages="he">, and is held to that instead.linter_mutations.pyhas twelve red mutations and six must-stay-green cases for them; one of the reds found that the starter's ownwindow.__manaurum.dir = dirwas being counted as the document write.preview.py.?locale=en|ru|hesends the matchingdirinmanaurum:init; the bar has an en / ru / he switch that postsmanaurum:locale-change,&switch=heposts one by itself after ready, and the shell posts one on ready as the real shell does. A new badge reads the framed page's<html lang>and computed direction back:language applied,language IGNORED, orfixed languagefor an app that declares one, also on<body data-locale-check>.smoke_tools.pychecks the payload per locale and, in Chrome with the browser pinned toen-US, that the starter reads green in Hebrew, Russian and after a live switch, a copy that ignores the payload reads red, and the patterns page readsfixed.patterns/index.htmlis English only, and says so withdata-languages="en": its sample text has no Russian or Hebrew, and English laid out right to left is wrong for a reader and a screen reader alike.- Docs.
sdk-api.mdhas a new section, "The person's language": where the setting lives (on the account,user_profiles.preferred_language, and in the shell's own cookie), how a window gets it, what the app does with it, the standalone fallback, and plainly what cannot learn it - an app's server, the Assistant's/agentcalls and a standalone tab - because Core does not put it inuser_context, the person pass or any capability (MAN-3244, listed inscripts/open-claims.txt).design.mdgains a "Right to left" section and aNeverrow for physical sides;checks.mddescribes the new rules, the fourth badge and the Hebrew screenshot;SKILL.mdStep 2.5 gets a fourth non-negotiable and three lines in the handshake snippet.
3.15.0 - the app skill is half as long: the path stays, the detail moves to the references
Summary: The main app-building guide is now half as long, so the AI reads less before it starts; nothing was dropped, the details moved to reference pages it opens when needed.
Why
skills/manaurum-app/SKILL.md had grown to 827 lines and 9,727 words, and an agent loads
all of it on every app request, before it has asked the person a single question. Most of
that length was not the path but the reasons for it: incident stories, the full Step 3.5
procedure with every measured browser quirk, the check_app.py findings table, the
deploy rejection codes, and paragraphs that repeated what v2-platform.md, sdk-api.md
and design.md already said. The rules that get apps rejected were in there, but an agent
had to read past 9,000 words to be sure it had them all.
What changed
SKILL.md: 9,727 → 5,020 words (827 → 537 lines). It keeps the path in order — Step 0, the reference apps, the seven rules (stories trimmed), the project layout and token rule, the minimal manifest and theapi_routesdefault-deny rule, the port rule, the handshake script itself with its three non-negotiables (the sender check against exactly the two shell origins plus loopback, an answer within 10 s, appearance frome.data.payload), the capability headers, every mandatory step with its commands, and Step 4's three carry-overs. Every heading other files cite is unchanged, and the "How to use this skill" map now listsreferences/checks.md. Each place that lost detail points at where it went.references/checks.md(new, 2,638 words) - Steps 3.5 and 3.6 in full: all five parts of the UI check with the--virtual-time-budget, ~500px viewport-floor, wide- and narrow-window and fragment details; whatcheck_ui.pycovers rule by rule; thecheck_app.pyfindings table and what it cannot see; the documentation rule behindtests/test_documented.py; and the gateway and capability error codes those checks prevent (deploy codes stay inmanaurum-deploy/SKILL.md, which owns them).references/v2-platform.md(8,965 → 9,770) -api_routesprecedence and "one rule covers every verb"; theapp.listen(80, 'localhost')case and the traffic path; Core's framing and CSP header rewrites; no host volumes; what the deploy packs (the exact exclude list, why.env*must live one level up, why.dockerignoredoes not help); the static nginx Dockerfile; the five deploy stages, ~8 s, no Core PR; the developer-token andmnu_*rules;permissionsdetails (standalone URL unaffected, what a still photo needs); other tenants are a 403.references/sdk-api.md(3,298 → 3,563) - why the handshake letsmanaurum:session-*through, what the loopback line is for, why the appearance is applied in the same listener, what the starter'sindex.htmladds, and the app that pinned only the apex.references/design.md(4,968 → 5,220) -window.print()/beforeunloadand the native-dialog replacements; no downloads, new tabs or clipboard writes and what to do instead; the small-window console check before a deploy; the "second app a week later" history of the seven rules.references/discovery.md(2,194 → 2,276) - why every screen gets a URL fragment in Step 0.references/capabilities-reference.md(7,769 → 7,892) - the Node call example, the private-files vs the user's Drive paragraph,os.kvis FORCE-RLS.- Every fact taken out of
SKILL.mdwas checked against the old text and lives in one of these files, or already lived inmanaurum-deploy/SKILL.md; nothing was dropped.scripts/check_repo.pynow requires the two measured Step 3.5 facts (drop--virtual-time-budgetfor a loading state; the ~500px headless viewport floor) inreferences/checks.mdas well.
3.14.0 - screens people read, filters that stay quiet, and an accent budget (from PR #27)
Summary: Apps made for reading now get their own layout, filters stop shouting in the accent colour, and the checks warn when too much of a screen is coloured.
Why
PR #27 (cut against 2.11.0) came from dindex-kb, a knowledge base over 1679 posts that
passed every check and was rejected on sight. The author had copied the shape of a
list-triage app, as the skill says to, and a reader came out looking like a ledger: titles
one typographic step above their captions, metadata on the right, thirteen category
filters built from accent-coloured .btn-ghost. Each rule was obeyed to the letter -
one primary button per view held while over twenty accent-coloured things sat on the first
screen, and "a badge is a word" held for a badge on 816 rows of 1679. Nothing in the SDK
named the difference between an app you read and one you sort through, and app.css had
nothing for the first kind. The PR sat unmerged and conflicting while main moved to 3.x;
this release ports its design half. Its database half shipped separately (3.13.0).
The same PR found a starter bug that is still on main: the cards inside a [data-view]
touch. .app spaces its own children, and since the router (2.9.0) those are the header
and the views. Measured through preview.py in headless Chrome: 0px between the
starter's overview cards before this release, 16px after.
What changed
app.cssgained the reading half, every class mobile-aware:.row.row-textwith.row-headline/.row-excerpt/.row-foot;.reader,.article-title,.article-meta,.lead,.prose,.pull;.chips/.chip/.chip-n(quiet until chosen, only the selected one accent, a sideways-scrolling row on mobile); the OS's own--lh-relaxed: 1.7(frontend/src/app/globals.css:258);ain the accent instead of the browser's blue; a capped, truncating.row-meta; and.app > [data-view]with.app's own rhythm - the touching-cards fix. A labelled search.fieldin a.toolbarnow takes the width the bare input has; it still does not grow (MAN-2849's rule stays).templates/patterns/index.html(new) - the screens the starter does not show, built only fromapp.css: a list of texts with chips, one article, a list of records with one badge on the one row that needs it. Its head script keeps the shell-origin check (MAN-2506) that PR #27's predates. CI holds it tocheck_ui.py.references/design.md- "What kind of screen is it" (sorting, reading, entering, and what each is built from), "Accent is a pointer, not a paint", fourNeverrows, every new class in the pattern table. SKILL.md: Step 0 asks for the kind of every screen (recorded inBRIEF.md§2); rules 3 and 4 add "it marks the few" and "at most four accent-coloured things on the first screen"; Step 3.5 reads the new bar and ends intemplates/design-review.md(new), five questions answered in writing per screen.discovery.mdmaps the kind in §2 to a layout;reference-apps.mdsays none of the three reference apps is a reader and points at the patterns page.check_ui.pyfails on an accent class (btn-primary,btn-ghost,badge-accent) handed out inside a loop, and on more than four in one view ofindex.html. Unlike PR #27's version, loops are read only from scripts and inline handlers, so "for (most) teams" in a paragraph is not a loop.preview.pymeasures the rendered first screen and shows it beside main's three badges: accent count (red above four), a badge on more than half the rows of a list (red), where the first list row starts; transitions are frozen while it reads, or a non-default accent counts 0 mid-fade.- Tests:
linter_mutations.py- three red mutations and three that must stay green (chips toggled in a loop, four accent things in a view, "for (" in running text).smoke_tools.pydrives a real headless Chrome through the meter: the patterns page must read green, a copy with thirteen ghost filters and a badge on every row red on both. - A flush card is flush on mobile too.
body[data-device="mobile"] .cardoutranked.card.card-flush, so on a phone every list row sat 16px inside its card and the hairlines stopped short (the starter's capabilities card, and the patterns lists). Measured in headless Chrome at 390px withdevice: mobile: 16px inset before, 0 after. - A toolbar in a view is spaced once. A
.toolbardirectly in.appor a view kept its own bottom margin on top of the column gap: 32px above the card in the starter's detail view, 16px now. A toolbar inside anything else (with chips under it, in a.reader) keeps its margin. - Review fixes before release.
preview.pyswaps the accent for a placeholder colour while it counts: graphite in light equals--text-tertiaryand green in dark equals--color-success, so captions and success badges read as accent (the patterns list read 24).check_ui.pycounts accent classes only inclass="..."values outside<style>and<script>(the primary-button count too), adds a page header above the views to each view's first screen, and in a loop no longer flags a selector,classList.removeorclassList.toggle(cls, condition). The starter's capability rows lose their "granted" badge - on every row it marked nothing. The patterns page's order rows are inert, so they loseis-interactive; its post rows answer Enter and Space. Mobile text rows keep 16px.smoke_tools.pyadds graphite-light and green-dark meter cases and fails if the two accent budgets drift.
3.13.0 - a database template that lasts past one request, and search that answers
Summary: Apps that keep their data in a database get a ready-made, tested starting point, including a search box that finds something when a question is phrased loosely.
Why
PR #27 (2.12.0) carried a Postgres recipe and three linter rules for it, and sat unmerged and conflicting while main moved on to 3.12.0. This ports that part of it - the recipe, its CI job, the rules and the two reference sections - onto the current tree, with every platform fact re-checked against Core main. The rest of PR #27 (screen kinds, the patterns page, the design review, preview's first-screen meter) is not in this release.
The bug the recipe exists for is asyncpg's, not the platform's: the pool runs RESET ALL
on every connection it takes back, so a SET search_path in init= lasts one request.
On the platform the role's default search_path hides it; on any other Postgres the app
fails with relation ... does not exist on the second request. Several first-party apps
shipped it until MAN-1443.
What changed
templates/recipes/postgres/(from PR #27):db.pyputssearch_pathinserver_settings, which survives the reset, with the platform's own value (the schema, oneext_<name>per granted extension,pg_temp) and the first-use pool and timeoutsv2-platform.mdalready teaches.search.pyruns full-text search strictly, then relaxed (or) when that finds nothing, says which one answered, and HTML-escapes thets_headlinesnippet before marking it. Three migrations: the table, a weighted generatedtsvectorcolumn, and its GIN index builtCONCURRENTLYin a file of its own. The pytest suite runs against a real Postgres and includes the brokeninit=pool.- CI: a
postgres recipejob runs that suite againstpostgres:16-alpineand fails if it would only skip. The tools job byte-compilestemplates/recipes/too. check_app.py: three rules from PR #27, rewritten on main's SQL lexer - a sessionSETinside an asyncpgcreate_pool(init=...)(not whensetup=orserver_settingsre-applies that setting), code readingDATABASE_URLunder"data": {"none": true}, and a generated column on a function Postgres refuses there (array_to_string,concat, one-argumentto_tsvector, clock and random functions). The last one runs whether or not the deploy's validator is importable, because the validator does not judge volatility. PR #27's own CONCURRENTLY, 64 KB and SQL-reader changes are not ported: main's versions supersede them.linter_mutations.py: a red case and a must-stay-green case for each rule, including the whole recipe copied into the starter.v2-platform.mdsection 7: "Connecting from the container" and "Full-text search"; the role's search_path is stated as Core sets it. SKILL.md points to the recipe and lists the new rules in Step 3.6 and "What NOT to do". README lists the recipe.
3.12.0 - the plugin installs in Codex too (MAN-1439)
Summary: The ManAurum SDK can now also be installed in Codex / ChatGPT, using the same skills and templates as Claude Code.
Why
PR #34 added Codex packaging in September, cut at 3.0.0; it sat unmerged and conflicting while the skills moved on. Its README and setup edits are superseded by later releases; its manifests are not.
What changed
.codex-plugin/plugin.jsonand a portable rootplugin.json(from PR #34): Codex reads the sameskills/andtemplates/, nothing is copied.- README: "Install in Codex / ChatGPT Work", with what is not verified yet (an install in a fresh ChatGPT chat, MAN-1439) and what does not carry over (the skills' slash-command cross-references, the stale-copy check, the session-start update hook).
check_repo.py: both new manifests carry the plugin's version, or the build fails — PR #34's had stayed at 3.0.0.
3.11.0 - document the code in the edit that writes it, and the starter checks it
Summary: Apps built with the SDK now explain their own code as they are written, and the starter's tests fail if a function is left unexplained.
Why
roiduani proposed this in PR #18 (2.9.0), which sat unmerged while the skill moved on. An
app nobody can safely change is a cost the first author never sees: the docstring written
after the app works records the signature, not what a None meant.
What changed
manaurum-appStep 3, "Document it in the same edit that writes it": Google-style docstrings and JSDoc, spent on what the signature cannot say; never anchor a test on comment text. (Shortened from PR #18; it does not take the Step 3.5 number, which is the UI check now.)templates/v2-starter/tests/test_documented.py(from PR #18): anastwalk that fails on any undocumented module, function or class undersrc/. The five starter functions that had none now do (_ok,_fail,save_my_note,read_note,write_note);_failsays why it cuts at 300 characters (Core passes the model no more,v2_capability_dispatch.py).
3.10.0 - every capability's documented input, checked against its schema
Summary: The SDK's own checks now also catch a capability documented with the wrong input fields, before an app built from it fails.
Why
The worst findings of the audit were inputs: os.files.upload without its required
size_hint, os.ocr.extract sending key for file_key, os.apps.call with fields Core
does not have (K2–K4). 3.3.0 rewrote the reference by hand; nothing kept it right. This is
the last item of the audit's contract plan (section 3, item 2).
What changed
scripts/sync_contract.pyrecordscapability_inputsintemplates/platform-contract.json: for each of the 32 capabilities, the input fields andrequiredof the schema its handler registers, read withast(module constants and**merges followed; a value it cannot evaluate is ignored, but a schema whose fields,required,oneOfornotit cannot read stops the sync).check_repo.py, incapabilities-reference.md, under a heading that names one capability: the Input: example (an inline{ … }, the fenced block below it, or the key-only form) may send only fields the schema has, must send every required one, and respects the schema'soneOf/not; a| Field | … | Required |table lists exactly the schema's fields, required where the schema requires them; an example in a shape the check cannot read is a finding, not a pass. Today 23 examples and 13 tables are checked and match Core; no sentence changed. Capabilities documented under a heading that names several (os.drive.list/.read/ …, the image and location pairs) are not checked; they were verified by hand against Core a5b9db9.linter_mutations.py: six more (145) — Core renames a field, drops one, adds one, makes one required, makes one optional, and requires one the example leaves out.
3.9.0 - manaurum-app reads in order, and each fact lives in one place (audit Н14)
Why
manaurum-app/SKILL.md gave no reading order, repeated the deploy script that
manaurum-deploy owns, kept a second copy of the container's environment table that had
already drifted from v2-platform.md's, and shared its trigger ("start building a new app")
with manaurum-setup.
What changed
- "How to use this skill" at the top of
manaurum-app: the steps in order — including the scaffold step that hands off tomanaurum-setup— and which reference to open for what. - Step 4 keeps the token and the three things to carry (echo the slug, the POST is
asynchronous, what
succeededmeans) and hands the script, the refusals and rollback tomanaurum-deploy, instead of a second copy of its script. - The environment table lives in
v2-platform.mdonly, now with what the skill's copy knew and it did not (MANAURUM_APP_IDis the app id header foros.kv.*andos.events.emitonly; theDATABASE_URLrole has no CREATE;MANAURUM_V2_TOKENis never injected). The skill keeps the three rules people get wrong and a pointer. manaurum-setup's description says what it is for — scaffolding files — and sends "what to build" tomanaurum-appand deploying tomanaurum-deploy.
3.8.0 - the error codes and the window protocol, checked against Core
Why
The audit's systemic recommendation was a copy of Core's contract that CI checks the documents against. 3.7.0 did that for the manifest schema and the capability list. Error codes and the window's message types were still trusted to memory, and both are what a developer matches on: a renamed code turns an error branch into dead code, and a message the shell stopped sending is a handler that never fires.
What changed
scripts/sync_contract.pyalso records the window protocol intemplates/platform-contract.json(messages: what a v2 app may post, fromV2_ALLOWED_MESSAGES; what the shell posts back, fromIframeAppHost.tsx; the v1 prefixes it refuses; the session runtime's two), and writesscripts/platform-strings.json: every snake_case word in a string literal of the Core code a developer's errors come from (gateways, deploy and credential routes, capability handlers, the Assistant's dispatch), read withast, docstrings aside, plus the literal tails of the f-strings an error is built from (detail=f"{prefix}_backslash"), so a built code still counts.check_repo.py: every error code a document quotes (`404 route_not_declared`,`502 upstream_error:<provider>`, or each code in the second cell of a status row) must still appear in those strings - which catches a code Core dropped, though not one renamed while its old name survives in some other string; everymanaurum:*a document or template names must be one the shell handles, sends or refuses; and every message in the protocol must be described insdk-api.md. Every quoted code and every message type matches Core today, so this release changes no sentence.v2-platform.mdnames MAN-3235 as the CLI release that will knowauth: "optional", with a line inopen-claims.txt, so--ticketslists it until it ships.linter_mutations.py: eight more (139), red and must-stay-green.
3.7.1 - what the 2026-10-02 audit left: stale pointers, the .dockerignore myth, untested rules
Why
After 3.2.0–3.7.0 the audit's critical, high and medium findings were closed but a tail of smaller ones was not, and one of them turned out to be wrong advice, not a stale pointer.
What changed
.dockerignoreis no defence on a platform deploy.manaurum-app/SKILL.mdandmanaurum-deploy/SKILL.mdrecommended it as a "second line of defence" against a leaked.env. Core builds with Docker's classic builder (POST /build,version=1,production.py), which does not apply.dockerignoreto the uploaded context, and the tar is stored as uploaded. Both now say: keep.env*out of the directory, and let the Dockerfile'sCOPYlist keep files out of the image. The starter's.dockerignoreheader, which said "anything listed here never reaches the builder", and itsrequirements-dev.txtsay the same.- The published CLI refuses
auth: "optional".cli-v0.3.0was cut on 2026-09-02, a month before MAN-3200; its schema allowsuserandanonymousonly, soapp validateand the deploy preflight refuse a manifest 3.6.0 teaches.v2-platform.mdsays so and gives the way round (--skip-preflightaftercheck_app.py, or the API). - Usage numbers exist (audit С28). README's "No metrics" now describes
GET /api/app-usage/<uuid>(MAN-3131: the platform's app UUID, a signed-in session) and says browser-error capture (MAN-3132) is for Aurum Studio apps only. - The starter waits as long as Core does (Н2):
capability.timeout_for()givesos.ai.*andos.ocr.*185 s andos.http.*/os.apps.*35 s instead of a flat 15 s, with tests. verdantis the ninth accent (Н9;globals.css,preferences.py): inapp.css,design.mdandpreview.py;SKILL.mdsays nine.sdk-api.md(Н10):manaurum-v2.mjsexportsfindClippedContentas well asManaurumV2; the layout guard,app.checkLayout()andinit({ layoutCheck: false })are described;IframeAppHost.tsxline references updated.- Pointers and leftovers (Н7, Н8, Н12, Н13, С15): "19 passed" in
manaurum-setupand the starter README (check_repo.pynow refuses "N passed" too, and reads the starter README); a link to a section that does not exist;production.pyandagent/types.pyline numbers; "dev(in-browser editor)" — that builder is gone; the slug rule inmanaurum-setup; "ondokploy-network" (the shared app network, whatever it is named); the starter README telling people to setmigrate_command; the build-context scan's refusals (a file over 32 MB among them), which only anenforcescan makes, in a row of their own. - CHANGELOG (Н15): every release heading is
#now; 2.0.0 and older used##. check_repo.pyrefuses "noworkspace_id" in the token, the last phrase on the audit's ban list;open-claims.txtre-verified against Linear.linter_mutations.py: 15 more mutations (131), one for each linter rule that had never been seen to fail (Н1): a module that does not parse, no Dockerfile, migration numbering, an unparseable manifest, short hex,rgba(),style=colour,<button class="row">, a clickable row withoutis-interactive, noindex.html, two primaries in one view, device and payload never read.
3.7.0 - the linters check what the platform checks, and the plugin can see itself drift
Why
The 2026-10-02 audit found two things wrong with the tools, beyond the documents.
- The linters were kinder than the platform.
check_app.pyaccepted{param}and a bare*inruntime.api_routes(the gateway matches both literally, so the route 404s), missed routes mounted withinclude_router(prefix=)oradd_api_route, passed an/agent/*handler whose only "check" was a word in a comment, flaggedoptional_capabilitiesand capability names in a README, never knew which capabilities exist, and let through most of what the deploy refuses: root-key typos, reserved and invalid slugs, write-named tools declared read-only, tool names the Assistant drops, and, without the CLI installed,BEGIN,SET,COPY,CREATE EXTENSION,RENAME,DROP INDEX,.SQLand the 64 KiB cap.check_ui.pyflaggedui.confirm()andstyle.width, and its appearance check passed an app that never applied the shell's appearance. - Nothing compared the plugin with the platform. Every stale fact had the same history: Core changed and no program here held a copy of what Core says. And the version hook only compared directories in the local cache, so a machine stuck on 2.7.2 with nothing newer on disk heard nothing for six weeks.
What changed
templates/manifest_v2.schema.json+templates/platform-contract.json: a copy of Core's contract with the SHA it came from — the schema, the 32 capabilities, reserved slugs, the slug pattern, the write-verb prefixes, the Assistant's tool-name format.scripts/sync_contract.pyrefreshes it from a monorepo checkout, read-only.check_app.pyreads the contract instead of keeping its own lists, matches routes exactly asapi_route_matcher.pydoes, followsinclude_routerandadd_api_route, decides/agent/*protection from the AST (aDepends/Securityon the handler, its decorator, its router or its include, followed through wrappers andAnnotatedaliases across modules), and adds the slug, root-key,auth-mode, tool-name, write-verb,entrypointandbyo/permissionsrules and the migration ones above. What it cannot trace (an include in a loop, a computed prefix,app.mount) it says as a note instead of guessing. FastAPI's own security schemes (OAuth2PasswordBearerand the rest, however assigned or imported) never count as a check: they read headers the gateway strips, and the runtime calls/agent/*withX-Manaurum-User-Contextonly (agent/v2_capability_dispatch.py). A library dependency is taken silently only when its name says user context or claims; one that says only auth, user, token, verify or the like is taken and named in a note. Migrations are read through a small SQL lexer, so a plpgsql orBEGIN ATOMICfunction body, a string or a comment is not a statement: anupdated_attrigger is no longer "transaction control", and'Rename it'is not a RENAME.DROP EXTENSIONis destructive (aDropStmtto Core), not forbidden; a function in a language other than sql or plpgsql is forbidden; a file that starts with a UTF-8 BOM is refused, as the deploy's parser refuses it; a subdirectory undermigrations/is a note (the deploy skips it); the 64 KiB cap is measured on the concatenation exactly as the deploy builds it.check_ui.py: only the globalalert/confirm/prompt, in scripts and inlineon*=handlers; only a colour set throughelement.style; appearance must be written from a value read off the payload (directly or destructured) by code that is used - called, or passed by name as a handler.check_repo.pyholds every document and every text file undertemplates/to the contract — each registered capability documented, no unregistered one named, thepermissionsenum as the schema has it,check_app.py's fallback keys equal to the schema's — and refuses eleven facts found stale, wherever they come back. "An earlier version said..." excuses its own sentence only, not the paragraph around it.version_check.pyalso reads the marketplace clone and, at most daily, the released version on GitHub (2 s, opt-outMANAURUM_SDK_NO_UPDATE_CHECK), and says how to update and how to turn auto-update on. A failed check is remembered for the day too, and the whole fetch, DNS included, has one 2 s deadline.hooks.jsonfindspython3,pythonorpy -3.linter_mutations.py: 65 new mutations, red and must-stay-green, for every new rule.- README: auto-update, the contract files, what the checks now cover.
- The gateway drops a client's identity headers (MAN-3214, platform
c1a7ccc), continued from 3.6.0.v2-platform.mdand the starter'sauth.pystill said, in places, that the gateway passes the client's headers through. It now drops that header,X-Manaurum-Personand the system-call headers on every branch (_CORE_ASSERTED_HEADERSinroutes/v2_app_gateway.py). The starter still refuses two copies, as defense in depth;check_repo.pyrefuses the old sentence. - The contract is synced at platform
56ce52c, which includes MAN-3200:auth: "optional"is in the schema's enum, socheck_app.pyaccepts it (3.6.0 documents it).
3.6.0 - auth: "optional" and the person pass (Core MAN-3200, MAN-3214)
Why
Apps whose pages both guests and members open (a share link, a voting room) had to
mint their own pass on a user route and carry it on anonymous routes, because the
gateway had no "the user if signed in" mode. Core adds it as stage 1 of App people
(MAN-3212, D-160). Ship this release together with the Core deploy that carries it:
before that deploy an optional route fails manifest validation.
What changed
SKILL.md,references/v2-platform.md"Pages that guests and members both open":auth: "optional"replaces "there is no third mode". A member gets the usualuser_contextplusX-Manaurum-Person(aud=MANAURUM_APP_ID); a guest and a member of another tenant get neither and are indistinguishable; never a401.references/sdk-api.md"Sessions in a standalone tab": thestalesignal, and why aPOSTis not repeated.templates/v2-starter/src/auth.py:verify_person_passand theoptional_persondependency, with the audience, tenant,typand duplicate-header checks; six tests intests/test_auth.py.- The pass verifier requires the
audclaim (require_aud): python-jose skips the audience check for a token with noaudat all. Core does the same (sergeysuaib-ui/manaurum#2328). - The duplicate
X-Manaurum-User-Contextnote: Core now drops a client-sent copy (MAN-3214). The starter keeps refusing two headers; it costs nothing.
3.5.0 - the manifest, the gateway, the window and the Assistant, checked against the code (MAN-1452)
Why
The last part of the 2026-10-02 audit (docs/audits/): facts about the manifest, the
gateway, the window and the Assistant that had gone stale.
- "
/agent/*is on the public internet" in twelve places: the gateway has refused it since MAN-1432. The check is still needed, for a different reason: every app's container shares one network. - "
runtimeis not strict" (v2-platform.md:24, the copy 3.1.0 missed). - "
data.sharedis one cross-tenant schema with an isolation warning": it is managed mode under another name.connection_capis read by nothing;data.extensionswas not mentioned. - "
egress_allowed_hostsdrops everything else" and a0.0.0.0bug fixed in MAN-2263: the list is enforced byos.http.fetchonly. - "
entrypointon a hosted app is ignored" (it is a422);platforms.mobile.entrypointworks for v1 only; theofflineblock does nothing for v2. - "The rest of your CSP survives verbatim"; "an external
app.fetchis subject to the gateway's egress rules"; "tokens.csshas a dead hostname and 6 of 8 accents" (fixed in MAN-2367). - Not documented at all: the 57-character limit on slug plus tool name, the approval card,
the Assistant's 30 s timeout,
routing_hintsin the user's language, the gateway's other answers (app_not_found,app_disabled,path_traversal_rejected, the 30 supstream_timeout),/__manaurum/, the read-only runtime,locale/dir/manaurum:locale-change, the session wrapper's own answers, and that the token carries no role.
What changed
references/v2-platform.md: §1 (strictness,app_idrules,data,platforms,agent_capabilities,offline), the/agent/*warning, routing hints, approval cards, timeouts, the guest pass (HMAC-SHA256, constant-time compare, revocation, the other-tenant404), names and roles, a new "What else the gateway answers",byoentrypoint, egress.SKILL.md,references/sdk-api.md,references/design.md,manaurum-setup: the same facts where they repeat them; the setup example's reader declares"is_write": false./agent/*wording in README,check_app.py, the starter'sagent_routes.py, README and test docstrings.app.css: the tokens note.
3.4.0 - the deploy as it runs now: a probe, a migration gate, immutable versions, owner tokens (MAN-2600)
Why
The audit (docs/audits/) found the deploy pages describing the pipeline from before
September, the same fact repeated in up to twelve places:
- "There is no readiness probe" and "a wrong port deploys green and then 502s". The probe
has existed since MAN-1369 and is on by default: a container that does not answer on
runtime.portfails the deploy and is rolled back. - "A failed per-tenant migration still activates the version". Since MAN-2510 it stops the version from going live.
- "Only three things fail synchronously; the manifest is validated in the job". The POST
now refuses a bad manifest, slug or owner (MAN-2597), a used version or another tenant's
slug (MAN-1586, MAN-1587) and an oversized archive (MAN-164) with its own
4xx, none of which were listed. - "Redeploying the same version is fine for dev iteration". It is
409 version_already_published, also after a deploy that failed once its image was pushed. - The token pages still taught
{"apps":["*"]}, a cap of 5 and "blank means all apps".*is refused, the cap is 20, and a brand-new app can only be deployed with an "all my apps" (owner) token (MAN-2597). - Rollback was "flips
installed_version_id, no body"; logs were "a stub"; the README pinned the CLI tocli-v0.2.0althoughcli-v0.3.0shipped on 2026-09-03.
What changed
manaurum-deploy/SKILL.mdis the single home of the deploy contract, rewritten against Coremain97d5660:- Prereqs: both token kinds, what a new app needs, lifetimes, the cap, where the CLI keeps the token, single ownership.
- The job: the result on success and on failure (
log_kind/log_tail), the phases in order, migrations before the container swap. - Every synchronous refusal of the POST, in order.
- What
succeededmeans: the migration gate and the readiness probe,health_pathstrict when declared. - Versions are immutable; when a label is used up.
- Migrations: the 64 KiB total, UTF-8, the 30 s / 5 s timeouts, and
migration.breakingapplying to every file. - Rollback (
version_label,409 deploy_in_progress, what it skips), logs (a real tail, not redacted). - Who gets the app after a deploy, and that another tenant's install does not serve its users today.
- Deleting an app: what is kept, and the redeploy-a-deleted-slug trap.
v2-platform.md§4–§6 keep the token facts in brief and point at the deploy skill; §7 and §8 are corrected (the gate, the install fan-out, App Store).SKILL.md,publishing.md,manaurum-setup, README,check_app.py, the starter's Dockerfile and README: the probe instead of "502", the synchronous manifest check,cli-v0.3.0.
3.3.0 - the capability reference, checked against the code one capability at a time (MAN-2144, MAN-2198, MAN-2995, MAN-1923)
Why
The 2026-10-02 audit (docs/audits/, in 3.2.0) found capabilities-reference.md
describing a platform from before September. Followed literally, it produced calls that
fail on the first request while the deploy stays green:
os.files.uploadwithoutsize_hint, which has been required since MAN-1707 (2026-08-13): a422on every upload.os.ocr.extractwith{provider, object_key, schema}; the input is{file_key, schema?}, and the output and errors were different too.os.apps.callwith{app_id, version: "1", timeout_seconds}; the input is{target_app_id, method, args, version: int, timeout_ms}, and only four methods of two built-in apps can be called. "RPC to another v2 app" (SKILL.md, v2-platform.md) does not exist.os.ai.completeandos.ai.embedanswering withusage; they answertokens_used,cost_usdandcost_known, so readingusageraised on every success.top_pwas documented as a passthrough and is a422. The page also describedos.ai.completeas BYOK only, before MAN-2412 bound it to the workspace's chosen backend.
Six registered capabilities were not documented at all (os.ai.providers,
os.ai.image_submit, os.ai.image_poll, os.locations.list, os.locations.get,
os.drive.delete): the page said "All 26" of 32. Others were stale: the Drive chapter
(5 MB, create-only, a notification MAN-2991 removed), os.compliance.audit_query ("scoped
to the calling app"; it returns the whole tenant's rows), os.apps.bulk_export (no
dataset is registered, so every call is a 404), event subscriptions (no hosted app can
receive events), notification limits, quotas, and three error codes that do not exist
(missing_provider_credentials, upstream_5xx, 422 egress_not_declared).
Two open PRs already carried part of this, written against 2.7–2.10: #17 (Drive) and #20
(camera, size_hint, the image capabilities, by roiduani). Their content is carried over
here and corrected where the platform moved since.
What changed
references/capabilities-reference.mdis rewritten against Coremain8fe6f5d, capability by capability, from the input schemas, return values and raise sites:- the inventory of all 32, and what does not exist;
- the call contract (which app-id form each family keys by, the user context, bodies
that are not JSON,
formatnot being enforced) and every gate the gateway runs, in order, with its code; - per capability: input with ranges and defaults, output, errors, limits;
os.ai.complete: which backend answers (unpinned,provider,modelonly), which workspace,log_prompt, the spend cap, and the error table;- new sections for
os.ai.image_submit/image_poll,os.ai.providersandos.locations.*;os.drive.writeoverwrite withif_match, andos.drive.delete; - a quotas section that lists the limits that actually fire.
v2-platform.md§3 stops repeating the contract (its error table was the stale copy) and points at the reference.provides/consumes, the wildcard grant and the dev-mode allow-list are corrected.SKILL.md: the capability table matches the reference;egress_not_declaredis a412;MANAURUM_APP_IDis the app-id header foros.kv.*andos.events.emitonly.permissionsis["microphone", "camera"]in every place that said microphone only (MAN-1920), with when a still photo needs no declaration and thebyorefusal (MAN-1922).scripts/open-claims.txt: MAN-133, MAN-2253, MAN-1289 and MAN-2199, which the new text cites as open.
3.2.0 - a token is checked for whose it is, and the window for who is talking (MAN-1307, MAN-3203)
Why
The audit of 3.1.0 against Core's main (285c8a8, docs/audits/) found three
places where the starter, and the text around it, taught something unsafe:
src/auth.pyaccepted any app'suser_context. It checked signature, issuer, audience and expiry, which every app's tokens pass: Core signs them all with one key for one audience. It never comparedapp_idortenant_id, and it defaulted missing claims to"". So a token the gateway minted for app A, which A's developer sees, opened app B for 60 seconds. The gateway also does not remove anX-Manaurum-User-Contextthe client sent, and adds its own under a different letter case, so a container can receive two headers, withheaders.get()returning the client's. MAN-1307 covers the binding on the platform side; the second header is not yet ticketed.- The handshake trusted the first sender. The starter's
index.html, the snippet inSKILL.mdand both insdk-api.mdacted onmanaurum:initfrom any window and answered it. Any page can frame a v2 app, every other app included, andmanaurum-v2.mjs2.3.0 adopts whoever postsmanaurum:initas its shell, on everyinit, and sends that window the app's Drive pick requests. The platform rule (MAN-2506) is the parent window and the shell's two origins.sdk-api.mddescribed the first-sender behaviour as correct. check_app.pycould not catch a real deploy token. Its pattern wanted 16 letters or digits straight aftermna_; Core mintsmna_<12 hex>_<secret>andmnu_<env>_<secret>, and the mutation that "proved" the rule planted a shape no token has. Generated in the real format, 400 tokens out of 400 passed the old rule. Meanwhilemanaurum-setupdrew.env.manauruminside the app directory, where the CLI packager uploads it, whilemanaurum-appsaid one level up.
The starter also said the capability gateway rejects a forwarded user
context. It requires one for os.drive.* and os.calendar.*, and
call_capability() had no way to send it.
What changed
templates/v2-starter/src/auth.pyrequiressub,tenant_id,app_id,app_version,expandiat; refuses a token whoseapp_idis notAPP_SLUG(401 user_context_wrong_app) or whosetenant_idis notMANAURUM_TENANT_ID(401 user_context_wrong_tenant, and503when that variable is missing); and refuses a request that carries the header twice (401 user_context_ambiguous).UserContextClaimsgainsworkspace_id, which the gateway does mint, andtoken, for forwarding.src/capability.py:call_capability(..., user_context=claims.token).src/static/index.htmlchecksevent.source === window.parentand the origin againsthttps://manaurum.com/https://app.manaurum.comin a listener registered before the SDK's, which stops whatever it refuses, so the SDK never sees it either. It letsmanaurum:session-*through untouched: Core injects a session-renewal script into every v2 page that talks to a Core frame of its own and checks those messages itself. On loopback the guard also admits the page's own origin, sopreview.pycan frame it. Checked in a browser againstpreview.py: a sibling frame'sinitandtheme-changeare dropped, itssession-responsereaches the next listener, and the shell's messages work.- Tests in
test_auth.py,test_routes.py,test_capability.py(new) andtest_static.py. Theuser_contextfixture now mints the slug, the tenant and aworkspace_id, as the gateway does; it minted a UUIDapp_idbefore. Each new check (the app and tenant binding, the required claims,exp,iat, the duplicate header, the missing tenant, forwarding, and inindex.htmlthe parent, self, origin and propagation checks, the loopback exception and the session pass-through) was removed or widened in turn, and the suite went red each time. SKILL.md,references/v2-platform.md,references/sdk-api.md,manaurum-setup: every handshake snippet carries the sender check and replies to the checked origin instead of'*'. The four verification steps forX-Manaurum-User-Contextare written down once, inv2-platform.md. Neither page says the SDK may be trusted to pick its shell.templates/check_app.pymatches the token shapes Core mints, exactly enough that an identifier likemna_token_from_the_environmentis not one.scripts/linter_mutations.pyplants a real-shapedmna_*and a newmnu_*case; both survive the old pattern..env.manaurumlives one level above the app, everywhere. The setup tree and the.env.manaurumsection say so, anddeploy.shreads../.env.manaurumand refuses to run with one inside the app directory.templates/preview.pysendslocaleanddirinmanaurum:init, as the shell does, and its "NO manaurum:ready" badge names the origin rule.docs/audits/: the audit report and its evidence.
Not in this release
- The platform half: stripping a client's
x-manaurum-*headers at the gateway and minting one audience per app. Until that ships, the checks inauth.pyare the whole defence. manaurum-v2.mjs2.4.0 (MAN-2561) has not shipped;https://manaurum.com/sdk/still serves 2.3.0, so the pages keep naming 2.3.0 and keep the inline guard mandatory rather than optional.- The rest of the audit's findings: deploy, tokens, capabilities,
/agent/*wording and the linters' other gaps go in their own releases.
3.1.0 - what the first app ported to v2 found missing (Planning Poker)
Summary: Fewer false alarms when checking your app, and app settings are found again.
Why
Moving Planning Poker onto Platform v2 produced eleven points of feedback.
Checked against Core's main, most of them were gaps in these skills rather
than in the platform, and two were defects in the SDK itself:
check_app.pycalledruntime.public_pathsandruntime.health_path"a key the platform does not read". The gateway reads the first and the post-deploy probe reads the second, so every app with a guest page got a false red.- The starter's
src/capability.pysaid in a comment to send the slug to everything butos.kvandos.events, and then sent the UUID to everything.os.secretsandos.filesstore under the header as sent, andmanaurum app set-secretwrites under the slug. So an app grown from the starter read every secret set from the CLI as404.
Checking those turned up a third: since MAN-1899 (2026-08-23) runtime is
additionalProperties: false. Five places here still said it was not strict
and that a typo deploys green and does nothing. A typo is a 422 now.
What changed
templates/check_app.pyknows all elevenruntimekeys the schema declares, and says a stray one is a422.scripts/linter_mutations.pygains a must-stay-green case withpublic_paths,health_pathandresources. It was red on 3.0.0.- The starter sends the UUID to
os.kv.*/os.events.*and the slug, kept asAPP_SLUG, to everything else. Two new tests pin the slug to the manifest and the form to each family. references/v2-platform.md:- §2 lists the eleven keys and who reads each one, and documents
public_paths,health_pathandresources, which these skills had never mentioned. - New: "Pages that guests and members both open". There is no optional
auth mode, and the gateway strips
CookieandAuthorization. The section gives the signed-pass pattern two apps already use, and says thatuser_contextcarries no name. - New: "Streaming routes - the limits". The limits are 50 streams per (app, tenant) per process, 15 minutes per stream and 60 seconds of silence; the section covers what each means for a client.
- §7 explains why the runtime role cannot
setvaland how a migration realigns a sequence after an import that kept its ids.
- §2 lists the eleven keys and who reads each one, and documents
references/capabilities-reference.md: which app-id form each family keys by and what the wrong one looks like;404 secret_not_foundnamed; drop credentials when anos.http.fetchredirect changes host, and declare the redirect's host.references/sdk-api.md: a new "Sessions in a standalone tab" section covers the 15-minute cookie, thefetchrenewal Core injects (MAN-2541), and what it does not cover (EventSource,anonymousroutes, anAuthorizationheader of your own).SKILL.md"What will bite you": the window has no downloads, new tabs or clipboard writes, and says what to do instead of each.manaurum-deploy: themna_*token, not the manifest, decides the tenant a deploy lands in, and how to check it before the first deploy.
3.0.0 - the v1 app path is retired (MAN-3021)
Why
On 2026-09-28 the platform retired the v1 path for third-party apps (decision
D-144). The iframe-bundle deploy, POST /api/dev/apps/deploy, has answered 404
since 2026-08-05, and the public v1 SDK files under manaurum.com/sdk/ other
than manaurum.js redirect to manaurum.com/developers#v1-retired. These skills still taught that
path in a "Legacy v1" section at the bottom of each one, and still called it
supported. An agent following them would build an app nothing can deploy.
Removing a documented path is a breaking change, hence the major version.
What changed
- Removed the v1 path everywhere: the "Legacy v1" sections of
manaurum-app,manaurum-setupandmanaurum-deploy(Manifest v1, themanaurum.jspostMessage SDK with its storage and db bridge, the zip-bundle deploy, the per-tenant catalog,mnu_*deploy tokens and their housekeeping),references/manifest-spec.md, the v1 half ofreferences/sdk-api.mdand ofreferences/publishing.md, andtemplates/legacy-v1/. mnu_*is described as what it is now: a tenant token for MCP clients and Drive upload, minted with explicit scopes and unable to deploy. Deploys use anmna_*credential throughmanaurum app deployorPOST /api/dev/v2/deploy, and nothing else.- No link to a removed public file remains.
manaurum.jsis still served for apps that have not moved yet;sdk-api.mdnames it once, to say it is retired and must not be loaded. mna_*credentials are minted at Dev Hub → Credentials (the old "v2 Tokens (Beta)" tab name is gone), and the shell's answer to a v2 frame'smanaurum:ai-*(rejected with an error reply) is documented.- Frontmatter,
plugin.jsonand the README describe a v2-only plugin.scripts/smoke_tools.pystops parsing the deleted v1 manifest.
2.14.0 - a database that comes up late is retried, not remembered (MAN-3008)
Why
On 2026-09-25 the provider's hypervisor starved the prod VM of CPU for ten
minutes, and Swarm recreated about half the containers at once. The v2 apps
were up at 18:07:08; the main Postgres accepted connections only at 18:07:58.
Five hosted apps open their pool once in lifespan and catch the failure into
a "no database" mode. They served empty 200s behind a green /healthz for 43
hours, and Postgres logged nothing, because the apps had stopped asking. A user
found it.
These skills never said how to hold a connection. DATABASE_URL was described
as a credential, and what an app does when the database is not there yet was
left to each author. The apps that got it right (a pool opened on first use)
did so by accident of whichever app they were copied from.
What changed
skills/manaurum-app/references/v2-platform.md- a new section, "Your database can come up after your container": the rule (never remember a failed connection), the ten-line lazy pool with a lock and a connect timeout under the gateway's 30 s, and the two shapes that look careful and are not. One swallows the failure; the other crashes, and takes the UI and/healthzdown with it until the database is back. The same rule covers a secret or anything else fetched from Core at boot.skills/manaurum-app/SKILL.md- one line in "What NOT to do", pointing at that section.
2.13.0 - a capped page is a centred page, and something checks it (MAN-2849)
Why
The starter's .app set max-width: var(--container-lg, 1024px) and nothing
centred it. Every app built from the starter sat on the left edge of any
window wider than 1024px, and four of them - zapiski, zb-announcer,
dindex-kb, zb-meetings - carry the line verbatim, because the file is
copied as-is. An owner found it within a minute: dragging the window wider is
the first thing anyone does.
It passed the whole prescribed check. check_ui.py had no geometry rule.
design.md stated half the rule ("cap the width") in prose. And the Step 3.5
screenshot did show it - at the default 1240px the starter leaves 0px on the
left and 216px on the right - but a one-sided gap in a single picture reads as
"fine" to whoever is looking. The lesson is MAN-2455's again: what a person has
to notice in a screenshot is prose; what the preview measures is a check.
What changed
templates/v2-starter/src/static/app.css-.appgetswidth: 100%; margin-inline: auto. The toolbar search field stops growing (flex: 0 1 320px, full width on mobile), so it no longer flings its siblings to the far edge;.toolbar-spaceris the one thing that grows. And a provenance line,manaurum-starter app.css 2.13.0, whichcheck_repo.pykeeps equal to the plugin version: it is how a deployed app's source says which template it was copied from.templates/check_ui.py- fails a page root (the first layout element in<body>) that caps its width and is not centred bymargin/margin-inlineauto, by theleft: 50%+translateX(-50%)trick, or by a centring<body>. Only the root is judged: amax-widthdeeper in the page (the starter's own.empty-body, a fixed toast) is its parent's business.scripts/linter_mutations.py- two red mutations (the MAN-2849 line verbatim; the same defect in a<style>block) and three that must stay green: a fixed toast withmax-width: 80%, a defective rule patched by an appended block, and a root centred by<body>. The harness now supports must-stay-green cases forcheck_ui.py, not only forcheck_repo.py.templates/preview.py- a third badge:layout centred,layout OFF-CENTRE - 0px left, 216px right, orlayout fills Npxwhen the frame is not wider than the cap and so proves nothing about centring.- Step 3.5 - a wide shot (
--window-size=1920,1000), and "a red badge is a failure however good the picture looks". design.md- "cap the width and centre it", and a row in theNevertable.- A correction: Step 3.5, the seven rules and
What NOT to doall saidcheck_ui.pycovers every rule but rule 3. It never checked the first half of rule 5, a hover on something inert. The text now says so.
Not in this release
The deployed apps are not fixed by this, and cannot be from here: app.css is
vendored by copy and the platform has no channel into a live app's stylesheet.
That is MAN-1401's real subject. The backend's advisory lint does not get the
new rule either, because it is a hand-kept copy of check_ui.py that has
already drifted (MAN-2765); adding a rule to both by hand is the defect class
this release is about.
2.12.0 - the migration chapter stops hiding a rule (MAN-2624)
Why
An app author adding an index to a table that already exists met three checks before production and all three missed the same rule.
The validator refuses a plain CREATE INDEX on a pre-existing table and says
"use CONCURRENTLY". Do exactly that - add the word to the file you already
have - and the deploy refuses the file, because CREATE INDEX CONCURRENTLY
cannot run inside a transaction block and the rest of the file needs one. That
second rule lived only in the deploy executor: not in the validator, not in the
CLI's vendored copy of it, not on this page. So manaurum app validate-migration said green under a sentence promising "green here means
green there", and the refusal arrived in production - after the image was
pushed, which on immutable tags costs a version number (dindex-kb 0.6.0,
2026-09-19).
The platform side is fixed in the monorepo (MAN-2622 / MAN-2623 / MAN-2624): the rule is in both validators now, the pre-push gate reads files one at a time the way the runner does, and a blocked deploy says why instead of pointing at an event stream the operator has no job on.
What changed
references/v2-platform.md section 7:
- a new subsection, "A file that uses
CONCURRENTLYmay contain nothing else" - what Postgres refuses and why the platform resolves it this way, thatmigration.breaking: truedoes not override it, the worked three-file shape, and the do-not-write counter-example; - the context-sensitive tier note now says each file is analysed whole and
one file is all the validator sees at once -
0001_init.sqlcreating the table does not make a plainCREATE INDEXin0002additive; - the local-validation promise says "file by file in the same order", which is what makes it true.
templates/check_app.py:
- the migration rule defers to the deploy's own AST validator when a copy that knows this rule is importable, and reports exactly what the deploy will say. Stdlib-only still holds - the import failing is an ordinary outcome;
- the capability is PROBED, not read off a version string. The rule landed in the CLI without a version bump, and the wheel authors can actually install predates it, so "is it installed" was the wrong question: a stale copy would have silently replaced this check with nothing;
- it does NOT guess when no usable validator is there. A text test cannot
decide this rule -
'a -- b'inside a string literal eats the rest of the line,DETACH PARTITION "m_2024--old" CONCURRENTLYreads as a comment, and the word inside a string or a nested block comment reads as a request. Core settled this in MAN-2510: "regex-based detection is explicitly rejected: the AST is the contract." So the run prints a note naming exactly which rules went unchecked, andcleanstops meaning two different things; - a validator that refuses without a per-statement breakdown is reported rather than swallowed.
scripts/linter_mutations.py:
- a mutation for the new rule - the shape an author lands on by following the
validator's advice literally. It runs against a stub validator on
PYTHONPATH, which makes it deterministic AND gives the validator branch its first test: CI installs no Python packages, so without a double that branch is the one thing nothing ever runs. The stub is a test double, not a second opinion - the real verdicts live in the monorepo, behind pglast; - a mutation may name more than one acceptable wording, because one rule can be reported by either engine. Still a substring match: a mutation cannot pass on the linter saying something unrelated.
2.11.1 — os.notifications.send_to_user as the platform actually answers it (MAN-2516)
Why
The reference taught a request the gateway rejects and a response that hid the
one state that mattered. A hosted app built from it (zb-vip-crm, "tell me when
a new lead arrives") sent deep_link: {app_id, path} and got 422; after
switching to link it got 200 {"delivered": false} for every lead, counted the
200 as sent, and marked every lead "notified". Nobody was notified: in-app
notifications were dead for every hosted app on the platform, and the page never
mentioned that a 200 can mean "not delivered".
The platform side is fixed in the monorepo (MAN-2516): in-app notifications from
hosted apps are delivered, a platform-side non-delivery is a non-200, and a
delivered: false always says why.
What changed
references/capabilities-reference.md, the os.notifications.send_to_user
section, rewritten against the handler:
- the input field is
link(a string handed back to your own app on click), notdeep_link;titleis optional;datais accepted and not stored; - the output section says to read
delivered, and lists everyreasonadelivered: falsecarries — all of them about the recipient; - the error table uses the real codes (
user_not_in_tenant,integration_not_configured; the olduser_not_found_in_tenantandmissing_provider_credentialsnever existed) and adds the new ones:412 in_app_unavailable,429 notification_rate_limited(in-app: 10 per hour and 50 per day per app and recipient),501 sms_unavailable,502 provider_rejected/provider_unreachable,504 provider_outcome_unknown, each with whether a retry is safe; - how the click reaches the app (
manaurum:initpayload.deepLink, or amanaurum:deep-linkmessage), and that the SDK does not surface it; capability_not_grantedsays how an install ends up without the grant (a redeploy never widens an existing grant; strict grants withhold this sensitive capability even at first install) and names the way through today: the platform operator, until tenant admins get a grant screen (MAN-1112, registered inscripts/open-claims.txt);- the click section says to attach the
messagelistener synchronously at startup, because the shell sends the link once.
Also in the same file: the gateway section no longer says a wildcard "*" grant
allows everything. Wildcard grants were removed in MAN-1585.
references/sdk-api.md: the four mentions of SDK 2.2.0 now say 2.3.0, the
version manaurum-v2.mjs carries. Each statement was re-checked against 2.3.0
(it still reads neither granted_capabilities nor deepLink, and still has no
window-framing helpers).
2.11.0 — the backend contract gets a program that checks it (MAN-2533)
Why
MAN-2510 named the mechanism, and it is not specific to design:
An agent treats as contract what sits in a numbered step marked mandatory, and treats everything else as reference material for if there is time left.
2.9.0 acted on that for the UI: a mandatory numbered step, a machine check, and a library that resists the mistake. The rest of the SDK never got the same pass — so everything that is prose today is, by that mechanism, optional today.
The most expensive v2 failure is the clearest case. A route in code that no
manifest rule covers is described three times in the skill and was checked
nowhere, while being entirely decidable from the app's own source before a
deploy. The evidence that this is the gap and not carelessness: the acceptance
build for MAN-2510 wrote fifty tests unprompted, and one of them was exactly
this check — the agent wrote the SDK's missing linter itself, because the skill
warns about route_not_declared in prose and nothing catches it.
What changed
templates/check_app.py (new) — the manifest against the code. Stdlib
only, python check_app.py my-app, exit 1 on findings, run on the directory
the deploy packs:
- an
/api/*route noruntime.api_routesrule covers — including the/api/x/*-does-not-cover-/api/xcase, which is the shape where the detail screen works and the list screen 404s; - a declared route nothing serves;
- an
/agent/*handler with no user-context verification — that path is on the public internet with no gateway in front of it, and nothing will ever tell you; runtime.portdisagreeing with what theCMDbinds, and withEXPOSE(which is decoration — but a decoration that disagrees is what the next reader believes);frontend.entry_pointnaming a file that is not there;- any
.env*inside the app directory; - a capability called but not declared (403 at the first real use), or declared and never called (an over-broad grant a tenant admin is asked to approve for nothing);
migrations/: a non-.sqlfile, numbers of mixed width, an anonymousDO $$block, destructive DDL withoutmigration.breaking.
Routes and handlers are read out of Python decorators with ast — that is
the starter's stack, and importing the app would need its dependencies. For any
other language it says so and skips those two rules; the manifest, port,
capability, .env and migration rules still run, because those read files
rather than code.
Wired the way the UI linter was wired, because a tool nobody runs is prose
with a shebang: Step 3.6 (MANDATORY) in manaurum-app/SKILL.md, a line in
What NOT to do, a pre-flight in manaurum-deploy/SKILL.md that runs both
linters before anything is packed, a test in the starter that runs it against
the starter, and a CI job that holds the reference app to it.
templates/v2-starter/tests/test_manifest.py (new). Four tests over the
manifest the starter's own suite never had, and the first one is the one worth
copying: every /api/* route the running app reports is covered by a
runtime.api_routes rule. It asks the app rather than the source, so it also
covers a route registered somewhere no decorator scan would look. Plus: no
/agent/* path in api_routes (declaring it there configures nothing), every
declared agent_capability has a handler, and every capability declares
is_write — because omitting it is not the same as false.
scripts/linter_mutations.py (new) — twenty mutations, one per rule. A
linter nobody has seen fail is a linter nobody has tested. Each rule in
check_ui.py and check_app.py gets a copy of the starter with that one rule
broken and a demand for red, and CI runs the lot.
It immediately found a dead rule. check_ui.py's @media max-width check
only ever ran over .html/.js — and a media query lives in a stylesheet, so
the rule could not fire on the one file type that carries it. It had been dead
since 2.9.0 and no amount of reading found it; one mutation did.
And check_repo.py now gets the same treatment. 2.10.0 shipped it with no
negative test at all — the file whose own docstring tells the story of a regex
silently disabled by one byte. Eighteen more mutations cover it, and half of
them are the other direction: prose a person would legitimately write, with a
demand that it stays green. Each of those was a real false positive first:
Never write the build context to `/tmp/ctx.tar`— the sentence teaching the MAN-2456 lesson could not be written, and neither could/tmp/ctx-$$.tar, which is the fix rather than the defect.- "Write two tests for every capability you use" read as a hardcoded count of this repository's own suite.
- "
check_ui.pycatches one of the two ways a hex reaches the markup" produceda hardcoded count of what the linter checks ("one of the"). - "MAN-2532 is Done, but it was blocked on a runner for two days" demanded a register entry for a closed ticket — which would have filled the open-claims register with closed tickets.
- "
scripts/deploy.shin YOUR project" was reported as a missing file in this one. - A
.zipand a.jpegfailed the build withcontrol byte 0x03 … rewrite the file with a real editor, not a shell heredoc. The binary test is now a NUL byte rather than a list of extensions somebody has to maintain.
Also real, and fixed: a !.env.manaurum line in the starter's .gitignore
satisfied the coverage check while git would have tracked the token file
(gitignore's last-match-wins is not fnmatch); doc.parent.parent let a file
outside the checkout satisfy a documented path; add_argument("-p", "--port")
made the documented --port report as unsupported; and a ; in the sentence
splitter separated a ticket from its own marker.
smoke_tools.py was testing whatever was on port 8766. A fixed port plus
"it answered 200" ran the entire suite against an unrelated HTTP server and
produced nine findings blaming preview.py, none of which named the real
cause. It now takes a port from the OS and refuses to run unless the page it
gets back is preview's own.
Three ways CI could pass while failing. count=$(pytest --collect-only -q | tail -1) swallowed a pytest failure entirely — GitHub's default shell is
bash -e with no pipefail — and wrote starter suite: with no number,
green; the same pipe hid a crash in the ticket listing. cancel-in-progress
applied to push: main too, so two merges in quick succession could land a
commit whose build was cancelled. And there is now a windows job: these
tools are maintained and run on Windows, check_repo.py makes a whole design
decision about the cp1252 console, and CI had never once exercised that path.
2.10.0 — the plugin can finally catch its own drift (MAN-2532)
Why
2.9.0 gave apps a linter. This repository still had nothing: no automated checks of any kind, and every PR merged with zero CI runs. The only detector of an instruction that had drifted from reality was an agent building an app, a customer rejecting it, and the agent writing a two-page ticket — which happened twice (MAN-2439, MAN-2510) before anyone noticed that this was the detector.
One afternoon of reading main on 2026-09-09 found nine slips, every one of
them mechanically decidable:
templates/v2-starter/src/static/app.cssusedvar(--container-lg, 1024px)with that token declared nowhere — the reference stylesheet breaking the exact rule its ownNevertable names;.gitignorecovered.envand.env.localbut not.env.manaurum, the one filename the skill tells you to create, next to a paragraph explaining that a leaked deploy token cannot be un-leaked;README.mdsaid "19 tests"; the suite had 24, then 27;SKILL.mdsaid the linter checked "nine of the seven rules", the CHANGELOG said ten, the truth was fifteen;README.mdsaid**Version 2.7.3**whileplugin.jsonsaid2.8.0;- both skills taught a deploy writing to
/tmp/ctx.tar— a fixed path in a shared directory, which is how two sessions crossed build contexts and one app was published over another (MAN-2456); .dockerignorecarried a comment describing an effect it cannot have;README.mdcalled MAN-1393 the blocker for the starter directory, months after MAN-1393 and MAN-1397 both went Done;- Step 3.5 described the opposite of what the browser does.
None of them is serious alone. Together they are the SDK lying about itself, and an agent has no way to tell which sentence is the stale one.
What changed
.github/workflows/ci.yml (new) — three jobs, on every PR and on main.
No secrets, no network beyond pip for the starter's own pinned dependencies,
and under two minutes.
- starter —
pytestintemplates/v2-starter, thenpython templates/check_ui.py templates/v2-starter/src/static. The reference app is held to the reference linter, which is the single line that would have caught the--container-lgbug on the day it landed. The test count is printed into the job summary, so it lives in exactly one place. - tools — byte-compiles what the skill tells you to run, feeds
check_ui.pya deliberately broken copy of the starter and fails if it stays green (a linter never shown to fail is a linter nobody has tested), and runsscripts/smoke_tools.py. - docs —
scripts/check_repo.py.
scripts/check_repo.py (new) — the documents against the repository.
Stdlib only, path:line: message findings that always name the file first.
Eleven checks, each one a slip from the list above: one version string across
four files; every templates/… / references/… path a document names exists;
every quoted § "Heading" citation resolves; every Step N reference has a
Step N; no hardcoded count of tests or of what the linter checks (and the
"seven rules" heading is checked against the list under it); no control byte in
any source file; no fixed /tmp/<name> in a shell recipe; the starter's ignore
files cover .env.manaurum and the .dockerignore still carries the note
correcting its own migrations/ line; every documented flag is one the tool
accepts; and two measured facts that live in two files at once must still agree
there.
scripts/smoke_tools.py (new). preview.py is a server a MANDATORY step
tells you to start, and version_check.py is a hook whose every failure path is
a deliberate silent success — so a hook that raises on line 3 looks exactly like
a hook with nothing to report. This starts both. It checks preview's four
fixture behaviours (exact, longest /prefix/*, method-qualified, and the
{"status": 500} envelope), because those are what make empty, could not
load and still loading three different screenshots instead of one; and it
checks that version_check.py prints nothing when the copy is current.
scripts/open-claims.txt (new). Nothing offline can tell you MAN-1393
closed. So every sentence in a live document claiming some ticket's work is
still outstanding needs a line here with the state as last verified and the
date — enforced both ways, so the file cannot rot into a list of closed
tickets. CI prints it on every run.
The drift itself, fixed. The /tmp/ctx.tar recipes now use a per-run
mktemp -d with a cleanup trap; the MAN-1393 and MAN-1397 claims are gone; the
three counts are sentences instead of numbers; a stale § "Legacy v1 deploy"
citation now names the heading that exists.
One correction that is not cosmetic. references/v2-platform.md still said
is_write was declarative only — the runtime ignores it for hosted apps.
That stopped being true on 2026-08-21 (MAN-1425/MAN-1872): the manifest value is
persisted and read, but the column is nullable and NULL is not false — a
capability that omits the key falls back to is_write=True, so a reader that
says nothing is journalled, confirmation-gated and excluded from cross-app
insight. Write "is_write": false explicitly. The starter's read_my_note now
does.
2.9.0 — a rule a program checks (MAN-2510, MAN-2455)
Why
2.8.0 put the design rules in the body of the skill, added Step 3.5 and shipped
preview.py. A week later a second app — technically flawless again, every
platform trap cleared on the first attempt — was rejected on sight again, by an
agent that had the rules in its context.
The mechanism is worth naming, because it is not carelessness: an agent treats
as contract what sits in a numbered step marked mandatory, and treats everything
else as reference material for if there is time left. Beside that sits
app.css, and copying it looks and feels like the design step being done — the
UI assembles, it resembles the template, every technical check is green. A rule
in another file loses to an artifact in your hands.
So 2.9.0 is mostly not new rules. It is checks, a library that refuses to build the wrong thing, and one honest promotion of design into the mandatory part.
What the second app shipped, all of it described in design.md already:
<button class="row"> instead of <li class="row"> (which is why the owner saw
"the list is not full width, the rows look like buttons"), a tab bar as
navigation, var(--text-muted, #666) with four token names that exist nowhere,
and a card per field. And a fifth, from MAN-2455: an app that answered the
handshake and read e.data.appearance instead of e.data.payload.appearance,
so it applied nothing and sat light inside a dark desktop for four days while
every check stayed green.
What changed
templates/check_ui.py (new) — the UI contract, mechanically. Fifteen
checks over the app's static files, covering six of the seven rules (a sentence
in a badge is the one only a person can see): a var() whose token is declared nowhere (a hex in
a fallback is a hardcoded colour wearing a token's clothes), hex or rgba() in
markup, style= and element.style.*, alert/confirm/prompt, a
tab/tabs/sidebar class, @media max-width, <button class="row">, a click
target with no is-interactive, more than one primary button in a view, and a
missing manaurum:ready / data-appearance / data-device / payload read.
Stdlib only, python check_ui.py src/static, exit 1 on findings.
Three things it does deliberately: it strips comments first (without that, the
sentence "never call confirm()" in a comment is itself a finding — that is how
its first run failed); it treats *.css and <style> blocks as the place
colours are allowed to live and everything else as markup; and it counts primary
buttons per data-view, because a hash-routed app keeps every view in one
file and each view is allowed one.
Step 3.5 is now (MANDATORY) in its heading, and the linter is its first
step. The only signal of obligation that an agent reliably reads is the one in
the heading — Step 2.5 had it, Step 3.5 did not. The linter runs before the
screenshots because it is cheaper and it catches what a picture cannot show.
The seven rules gained the two sentences that were missing. Rule 2 now says
where the values arrive (e.data.payload, not e.data) and what the failure
looks like, because an agent that already has a handshake reads "appearance
comes from manaurum:init" as "yes, I do that" and never re-opens Step 2.5.
Rule 5 now runs both ways: hover if and only if the click does something — so a
row with a handler must carry .row.is-interactive, and stays an <li>.
What NOT to do has a UI line. It is the only section on the page whose
title is "what not to do", so everything absent from it reads as "not what you
get sent back for". Six technical entries, and the thing that actually got two
apps rejected was not among them.
preview.py:
- fixtures match the way
runtime.api_routesdoes — exact, then longest/prefix/*. An exact-key dict could never answer/api/items/42, so the detail screen photographed as an empty card, every time; - a fixture may be an envelope —
{"status": 500},{"delay_ms": 1500, "body": …}— so that nothing yet, could not load and still loading, whichdesign.mdasks you to distinguish, are all photographable rather than one of three; ?width=/?height=size the app's frame inside a large browser window. The whole contract is written for a window that is "often 900px", the recommended shot is 1240×1000, and both headless browsers floor their own viewport at ~500px — so this is the only honest way to photograph the narrow case;- a second badge: appearance applied / appearance IGNORED, read back off the
framed page's
<html>. MAN-2455's bug fails this badge with no judgement call from anyone.
The starter stops making the mistakes easy to make. .row-title and
.row-sub get display: block (as <span>s they ran together on one line and
text-overflow: ellipsis did nothing); .row gets the button neutralisers that
.btn four blocks below it already had, plus a comment saying a row is an
<li>; a.btn loses the underline; .toolbar and mark exist (the second so
that search highlighting is not reinvented in the banned yellow); and where
.tab, .tabs, .sidebar and .switch would be there is now a comment
saying they are absent on purpose. A missing class and a gap in the library look
identical, and that is how a tab bar gets built.
Hash routing moved into the starter and into Step 0. index.html ships an
eight-line router, two data-views and a list whose rows are <li class="row is-interactive"> with one delegated handler and a keyboard path. The
requirement used to arrive in Step 3.5, when the app already exists and
retrofitting it is expensive — so it was skipped, and every screenshot only ever
showed the first screen.
references/design.md gained the token table, name by name, each with what
it is and which neighbour it is not. There was no list anywhere, so names were
invented by analogy — and an invented name in a var() fallback breaks no
stated rule while being exactly the hardcode the rules forbid. Plus the two
Never rows for that and for a silent click target.
The plugin now says when your copy of it is stale (hooks/hooks.json →
scripts/version_check.py). Measured in MAN-2510: a session loaded 2.7.2, the
cache received 2.8.0 fifty-one minutes later, and the session kept reading 2.7.2
paths for another day — the release meant to prevent that app's failure missed
it by an hour and was never noticed. The SessionStart hook compares this copy
against its siblings in the cache, says so in one paragraph when it is behind
(or when its directory carries .orphaned_at), writes STALE.md into the
superseded directory and a current pointer beside it — because SKILL.md
teaches agents to find the plugin root by walking the filesystem, and two
directories that differ only by a hidden marker are indistinguishable. It prints
nothing when the copy is current, and it cannot fail a session.
SKILL.md also states its own version at the top and tells the agent to look at
the parent directory of <plugin> before trusting what it is reading.
The deploy recipe stopped using shared filenames (MAN-2456). Both skills
told everyone to write the build context to /tmp/ctx.tar and the request body
to /tmp/deploy.json. /tmp is shared: on 2026-09-08 two sessions deploying
two apps on one machine collided on exactly this, and one of them shipped the
other's archive — phases streaming healthily, activated reported for an app it
had never touched, while its own app stayed on the old version. Now: a per-run
mktemp -d, and the slug echoed from the manifest before the upload, because a
run that reports success for the wrong app is worse than one that fails.
2.8.0 — the design rules travel with the skill, and somebody looks at the app (MAN-2439)
Why
An app built on 2.7.2 cleared every technical trap on the first attempt —
manifest v2, runtime.port, default-deny api_routes (including the
/api/x/*-does-not-cover-/api/x edge), managed Postgres, migrations, the
manaurum:ready handshake, a secret through os.secrets, deploy and job
polling. Its interface was then rejected on sight, and correctly: a tab bar as
navigation, badges holding whole sentences, a primary button in every row of a
list, and an app that stayed in its own palette inside a dark desktop.
All four are written down in references/design.md. That file was never opened.
The mechanism matters more than the miss. The skill pointed at design.md
twice, and both pointers read as further reading — while a complete, correct
app.css sat one directory away. Copying the stylesheet feels like the design
step: the UI assembles, it looks like the template, and every technical check
passes. Nothing in the skill said otherwise, and two things in it actively
helped the failure along:
- The handshake snippet in Step 2.5 dropped the payload on the floor. It
answered
manaurum:readyand read nothing else — so an app that followed this skill to the letter ignoredappearanceandaccentby construction. That is the third violation, with a recipe. - Nothing ever asked anyone to look at the result. Rules cannot survive a build that is never seen. All four failures were obvious in the first screenshot and invisible in the diff.
One correction to the report, checked rather than assumed: the starter template
does not ignore the theme. index.html writes data-appearance /
data-accent on <html> and app.css carries the dark block, all eight
accents and color-scheme — that landed in 2.7.0 (MAN-1436) and is present in
the published 2.7.2. Verified by framing the unmodified starter and
photographing it in both appearances. The gap was in the skill's own snippet,
not in the template.
What changed
-
manaurum-app/SKILL.md— "The seven rules an app gets sent back for." Seven lines in the section that already tells the agent to copy the look, before the first file is written: no tab bar or sidebar; appearance and accent frommanaurum:init; a badge is a word, not a sentence; one primary button per view; hover only on what is clickable; no hex or inlinestyle=in the markup; noalert()/confirm()/prompt(). The link todesign.mdstays, but the checklist works without following it. -
manaurum-app/SKILL.mdStep 2.5 — the handshake snippet now applies the theme. One listener, both jobs, because both arrive in one message; it also handlesmanaurum:theme-change. With a paragraph saying plainly that answering the handshake and discarding the payload is a shipped bug, not a shortcut. -
manaurum-app/SKILL.mdStep 3.5 — "Look at the UI before you deploy it." A mandatory last step of building an interface, in the same position as "check/healthz" after a deploy: serve with stubs, screenshot light and dark, then open the pictures and criticise them out loud against the seven rules. Includes the three things that make the procedure lie to you: headless cannot click, so a view reachable only through a button needs a URL fragment before a screenshot can reach it; a fresh--user-data-dirkeeps the run independent of an open browser profile, which is one of the ways the command exits writing no file and printing no error; and the layout viewport floors at ~500px, so--window-size=390,800crops rather than reflows and a good phone layout photographs as broken. Both browser claims re-measured for this release — Chrome and Edge,--headless=new. -
templates/preview.py+templates/preview-fixtures.json(new). A stdlib-only preview server, no install and no dependencies: your static files, a JSON stub for every/api/*call, and a/__shellpage that frames the app the way the desktop does — the shell's exact sandbox (soalert()is as dead as it is in production), a realmanaurum:initcarrying whicheverappearance,accentanddeviceyou ask for, and a red badge whenmanaurum:readynever comes back. It lives beside the app directory, never inside it: everything inside is packed into the deploy. Every API hit is logged, which is the cheapest way to find a route missing fromruntime.api_routesbefore it 404s in production. -
manaurum-app/references/design.mdnow opens with aNevertable. Nine rows, each a rule an app has shipped without and been sent back for, each with one line of why. The rules were all in the file already — spread through the prose of six sections, where they were visible only to someone reading the whole page. -
manaurum-deploy/SKILL.md— the pre-flight section now sends you to Step 3.5 first if nobody has looked at the app yet. -
README.md—templates/preview.pydocumented under Templates, and the "no local dev loop" gap corrected: the frontend now has one, the backend still does not.
Version note. The report expected 2.7.3 to be the os.drive.* work; it is not —
2.7.3 is the scroller rule (MAN-2112), and os.drive.* was documented back in
2.1.0 (MAN-608). Either way nothing was left unpublished: 2.7.3 has been on
main since it merged, and a plugin install caches per version, so a machine
sitting on 2.7.2 has a stale cache rather than an old release. /plugin →
update brings 2.7.3 and this release together.
2.7.3 — an app owns its scroller, and the SDK says so out loud (MAN-2112)
Why
The skill had nothing to say about scrolling. 3,816 lines across SKILL.md and
eight references, and overflow appeared in exactly one of them — about a query
cardinality cap. scroll appeared nowhere at all. So agents building v2 apps
kept shipping the same bug: content clipped at the window edge with no scrollbar
anywhere.
It is structural, not careless. The OS window's content area is overflow: auto and it does scroll a builtin app. It can never scroll an iframe app:
the iframe is height: 100% of that same box, so it is never taller than the box
and the shell's scrollbar never appears. An iframe app scrolls itself or it does
not scroll — and nothing said so. Meanwhile every design mockup fakes an OS
window with overflow: hidden, which ports perfectly while the inner scroller it
was paired with does not, because the real layout gets rewritten around it.
Finance v2 shipped exactly this on its desktop layout, from its first commit. The public P&L share page had the same failure in June (MAN-340). MAN-1050 is the same "one scroll container" rule broken from the other side — a nested scroller trapping the user, fixed by deleting it.
What changed
references/design.md, Window rules — "One scroll container, and it is yours". The first draft of this rule was wrong and review caught it before merge:overflow: autoon its own changes nothing, because a block with auto height grows to fit its content and never overflows. A fixed shell needs all three of a flex-column root,flex: 1andoverflow: autoon the element holding the content, andmin-height: 0on the flex items in between.manaurum-app/SKILL.md— an entry in "What will bite you", which already opens with the right frame: it works in a tab and breaks in the desktop.manaurum-deploy/SKILL.md— a pre-flight step with an actual procedure: the smallest window you support, enough data to overflow it, and the browser console as the observable.templates/v2-starter/src/static/app.css— the starter was already correct (min-height: 100%on.app, no clip on the root) by accident rather than by rule. Now the rule sits abovehtml, body { height: 100% }, so a port does not overwrite it silently.
Platform-side, in the same ticket: both SDK artifacts measure this at run time
and console-error with the offending element (manaurum.js 1.12.0,
manaurum-v2.mjs 2.3.0). The check is scoped to "a window-sized element clips
and nothing on the page scrolls at all" — verified in Chromium against seven
healthy layouts that must stay silent, including a collapsed panel, a tall line
clamp and a decorative hero, all of which an earlier revision flagged. Opt out
with init({ layoutCheck: false }); app.checkLayout() forces a measurement
even then.
2.7.2 — the dev runtime has no editor any more (MAN-1577)
Why
Manaurum removed the App Builder — the in-browser Monaco editor at slug
appbuilder — from the product on 2026-08-07 (MAN-1408). Aurum Studio is now the
only builder it ships. Four files in this skill still described that editor as a
live surface, in eight places, so an agent reading them would hand a developer a
path that no longer exists.
What changed
The dev runtime mode is not gone, and this release does not pretend it is.
runtime.mode: dev, the dev_apps / dev_app_files tables and the
/api/dev/v2/dev-apps/* routes are all still mounted in Core. What disappeared is
the only UI that drove them. So the docs now say exactly that, rather than
deleting sections that remain technically accurate:
SKILL.md—runtime.mode: devis described as a platform-internal prototyping runtime that no longer has an editor.references/v2-platform.md— thedevsection keeps its contract details under a banner saying no editor ships for it and you should targethosted.references/publishing.md— the session-cookie publish endpoint and its poll surface are relabelled "dev mode"; the table now records that the route has had no UI client since 2026-08-07. Themna_*CLI path is unaffected and is the one to use.references/capabilities-reference.md—capability_denied_in_dev_modeis described by the manifest field that triggers it rather than by the dead product name, and theKNOWN_CAPABILITIEScaveat now notes thatapp_builder_v2_capabilities.pyis a legacy filename for a live, shared file.
Historical CHANGELOG entries are untouched.
2.7.1 — the skill answers to what people actually say (MAN-1453)
Why
Everything 2.7.0 built sat behind a door that only opened for one word.
Measured on f004843 (2.7.0), eight sentences a non-developer would open with,
each in a fresh empty directory with the plugin loaded:
| skill invoked | |
|---|---|
| "i need an app to keep track of which of my plants ive watered" | ❌ |
| "i want something to track my freelance invoices" | ❌ |
| "can you build me a little tool for logging my gym workouts" | ❌ |
| "i need a place to write down what my clients ordered" | ❌ |
| "make me something to remember my kids school stuff" | ❌ |
| "i want an app for my shop" | ❌ |
| "build me a simple tool to track who owes me money" | ❌ |
| "хочу приложение чтобы вести учёт расходов" | ❌ |
0 of 8. Say "manaurum" and it fired every time; describe the problem and it
never did. The agent instead offered ManAurum as option 2 of 3 and recommended a
plain standalone page — in the "I don't know" run it built a 1095-line local HTML
file styled with data-theme, the one pattern design.md prohibits.
So the interview (MAN-1435) and the stylesheet (MAN-1436) were both unreachable by the exact first sentence they were built for. The target user cannot program; they describe a problem and never think to name a platform.
Changed
manaurum-app'sdescriptionnow triggers on intent, not just on the product noun. It keeps every existing trigger and adds the case that was missing: someone asking for an app or a tool to run part of their life or work without naming a technology, in any language. It also says out loud not to offer a standalone HTML page instead, and lists what still does not belong to this skill — work inside an existing codebase, a plain script, or a stack the user already chose.
After, same eight sentences, same conditions: 8 of 8. A four-sentence control group that must NOT match — a Python file-renaming script, a Next.js landing page, "explain how OAuth works", and a dark-mode toggle for a React component in the current folder — stayed at 0 of 4 before and after.
Note for anyone editing a description again
Two traps cost real time here, both silent:
- A double quote inside an unquoted YAML scalar removes the skill from the
list entirely. No parse error, no warning —
manaurum-appsimply stopped existing whilemanaurum-deployandmanaurum-setupstill loaded. If a skill vanishes, look at the frontmatter punctuation before anything else. - Do not measure trigger rates with
--disallowedTools Write Edit Bash. An agent that cannot write files declines the skill, so the first run of this experiment showed 0 of 8 after the fix as well and nearly buried it. Give the run full tool access and kill it on a timeout instead.
2.7.0 — ask before you build, and ship something worth looking at (MAN-1435 / MAN-1436)
Why
Two gaps, both measured on origin/main at 2.6.0, both about the same moment: what
happens when a person who cannot program says "build me an app".
Nothing asked them anything. Both skills opened at "copy the starter" / "write the
manifest". Read that as a missing process step and you fix the wrong thing — the person
is not withholding a spec, they do not have one and do not know what they are supposed
to tell you. So the agent invented the data model, the surfaces and the
agent_capabilities from one sentence, and the guess stayed invisible until the app
existed and was wrong.
The design guidance was not merely absent, it contradicted the file next to it.
references/design.md was 442 lines built around a two-theme world: 12 XP references,
app.mul.* (which does not exist in the v2 SDK — 0 hits in manaurum-v2.mjs), and 0
mentions of the v2 client SDK. Meanwhile references/sdk-api.md:59 — same directory —
already said the correct thing: theme is always smoothie inside an iframe, style off
appearance and accent. The plugin shipped both instructions and let the agent pick.
Three findings worth keeping, all verified against the monorepo rather than assumed:
app.onReady/app.onThemeChangeare live v2 API, not v1 leftovers (manaurum-v2.mjs:195,208). The defect indesign.mdwas never a dead call — it was semantics. It taughtonThemeChange(theme => body.className = theme). The shell sendsIFRAME_THEME, a hardcoded'smoothie'(IframeAppHost.tsx:127,309), and the SDK passes that constant to the callback (:126). An app copying that snippet setsclass="smoothie"forever and never reacts to dark mode. Onlyapp.mul.*was genuinely dead.- XP cannot reach an app at all — "the XP look stops at the window frame"
(
IframeAppHost.tsx:124). It is a shell easter egg for one tenant. Every XP style block in this plugin was code that could not execute, in v1 as well as v2. - The starter ignored the appearance signal entirely. It styled off
prefers-color-scheme, which tracks the browser. Measured: with the shell postingappearance: 'dark', the 2.6.0 starter stayedrgb(246,247,251)and never setdata-appearance. A user in OS dark mode got a white app in a dark desktop.
Added
- A discovery phase before any file is created (MAN-1435).
Step 0in both skills: one question at a time, plain language, and — the load-bearing move — propose what you think the app is after two or three answers and invite correction, because people correct a wrong guess far better than they specify from nothing. It is explicitly not a gate ("just build me a todo list" → draft the brief, confirm once, go) and it always terminates ("I don't know" → pick the default, record it, say so). templates/v2-starter/BRIEF.md— the spec, in the user's words, that they own and can edit. Six sections that each map to something concrete: §1 → route auth and visibility, §2 → screens andruntime.api_routes, §3 → the data model, §4 →agent_capabilities, §5 → guardrails, §6 → every guess the agent made, marked(assumed). Verified end to end: adding one line to §4 of a real brief produced oneagent_capabilitiesentry and onePOST /agent/<name>handler, with §5 forcing it toUPDATErather thanDELETE.skills/manaurum-app/references/discovery.md— the question bank, the defaults for the uncooperative case, the brief→manifest derivation table, and two worked transcripts: a vague one-liner reaching a five-status enum and two Assistant capabilities without a single technical question, and an "I don't know to everything" run that still terminates. Transcripts because a model imitates a transcript; it skims a rule.templates/v2-starter/src/static/app.css(MAN-1436) — a real stylesheet, and the half that actually changes what gets built. Tokens, page shell, cards, lists and rows, forms, four button ranks, badges, empty states, skeletons, focus rings, mobile. It uses the OS token names (--space-*,--surface-*,--radius-*,--accent) so adopting the shared system later is one<link>and no rule has to move.- All eight OS accents, copied verbatim from
globals.css. The publictokens.cssdefines six —amberandgreensilently fall back to blue there. tests/test_static.py— five tests for the contract that only breaks inside the desktop: the stylesheet is served,index.htmllinks what it ships, the handshake reply precedes the module bundle, appearance reaches the DOM on init and on change, and[hidden]is overridden. Mutation-checked: 7 of 7 deliberate breaks go red. The first version of this file let 2 of them through because it matched the explanatory comments rather than the code — the_code()helper and the comment in that file exist so the next person does not repeat it.
Changed
design.md: 442 → 216 lines. All XP styling andapp.mul.*gone. What replaced it describes what a v2 app actually is (an isolated iframe with its own CSS), gets the appearance/accent contract right, and adds what was missing entirely: layout and composition, so an agent with correct tokens stops inventing a page shape. It now points atapp.cssas the artifact instead of re-dumping CSS.design.mdis no longer filed under "Legacy v1" in either skill. It was reachable only from the v1 sections, so nothing on the v2 path ever read it.manaurum-app/SKILL.mdnow points at it and atapp.csswhere an agent decides what to copy.- The starter follows the shell, not the browser.
index.htmlwritesdata-appearance/data-accentfrommanaurum:initandmanaurum:theme-change, usingprefers-color-schemeonly as the standalone default. Measured after: shelldark+ browserlight→rgb(23,23,26); shelllight+ browserdark→rgb(247,247,249). Both directions, and the handshake still answers. - The starter's UI shows the patterns instead of describing them — a form with a real
empty state, a skeleton that resolves into a key/value panel, and a list of the install's
granted_capabilitieswith badges. That list is read from the rawmanaurum:initpayload because the v2 SDK does not exposegranted_capabilities(0 hits inmanaurum-v2.mjs), and it is the fastest way to see why a call returns403 capability_not_granted. templates/legacy-v1/theme-aware-app.htmladapts to appearance, not to XP. It was 21 lines of unreachable XP CSS,body.className = ctx.theme(always'smoothie'), the claim "adapts to both Smoothie and XP themes automatically", and no dark mode at all. It now uses the v1 SDK'sonAppearanceChange/onAccentChange, which existed all along.hello-world.htmlgot the same fix.- Two factual corrections in
sdk-api.md: themanaurum:themewire example showed a payload of{"theme": "xp"}the shell never sends, andapp.getTheme()was documented as possibly returning"xp".
Added in review, after running the skill from blank directories five times:
- Template paths now carry the
<plugin>/root marker.manaurum-app/SKILL.md,discovery.mdanddesign.mdpointed at a baretemplates/v2-starter/…, and in one run out of two the agent resolved that against the skill directory, gotFile does not exist, and silently wrote its ownapp.css, its ownBRIEF.mdand no tests at all — justifying the missing suite with "the reference fixture needs a real Postgres", which the starter disproves (24 pass with no Postgres and no Docker).manaurum-setupnever had the problem because it already wrotecp -r <plugin>/templates/v2-starter. The three files now match it, and the instruction says to resolve the root and retry rather than fall back to writing the file. - The
[hidden]guard is indesign.mdtoo, not only insideapp.css. An agent that writes its own stylesheet — which is correct and expected when the app is not Python — never sees the rule. Observed twice out of five runs: both re-derived sheets patched.modal-backdrop[hidden]by hand and without!important, which is exactly the case-by-case vigilance the global rule exists to replace. design.mdnow states the stance on sidebars, tab bars and toggle switches. All three had sections onorigin/main;app.cssdeliberately ships none of them, but nothing said so, which read as an omission rather than a decision.- The two
legacy-v1templates say what does not work. TheironAppearanceChange/onAccentChangehooks are correct, but the shipped v1 SDK never fires them — itsmanaurum:theme-changehandler aliases its own context and compares each value against the copy it just overwrote, so the guard is always false (MAN-1450). Verified against the livemanaurum.js: init applies, every subsequent change is dropped. Still a strict improvement — before this release neither template readctx.appearanceat all — but the comment no longer promises live updates the platform does not deliver.
Not changed
- v1 is still supported and its SDK calls still appear in the Legacy v1 sections. That
is what those sections are for.
app.onReadyin a v1 example is correct. is_writestays in the starter manifest. It is dead at runtime (MAN-1425) andv2-platform.mdalready documents that precisely while telling you to declare it truthfully anyway. Removing it from the artifact would have contradicted deliberate guidance; this release leaves the position alone.- XP mentions in this changelog. History is not rewritten. The remaining XP text in the skills is now exclusively "this cannot reach you, do not style for it".
templates/v2-starter/still exists. Deleting it in favour of the CLI scaffold is MAN-1393 item 4 and is blocked on a CLI release, not on an opinion.- The shared design system is still vendored, not linked — MAN-1401 is unresolved, the
URL
tokens.cssdocuments for itself does not resolve, and a failed<link>has no graceful degradation. Token names match so the swap stays cheap. - Aurum Studio. Out of scope by decision (2026-07-26): the terminal is the product here, and no shared core is being built.
2.6.0 — the artifacts teach, not the prose (MAN-1394 / MAN-1395 / MAN-1396)
Why
An agent holding this skill imitates the artifact it copies far more reliably
than the paragraph it reads. 2.5.0 shipped 4,339 lines of accurate prose next to a
starter that a real app has nothing in common with: no tests, no /agent/* handler,
one 200-line main.py. So the skill said "declare agent_capabilities" in three
files while the only copyable app declared none, and said "split by domain" while the
only copyable app was a single module. The artifact won, every time.
Three concrete costs, all found by building an app with the 2.5.0 skill and deploying it:
- The filename in every snippet was wrong.
manifest_v2.jsonappeared in 14 places across the three skills; the schema, the CLI and the platform have only ever acceptedmanifest.json. Copy any snippet verbatim andmanaurum app validatecannot find your manifest. - The shipped
deploy.shcould not deploy.jq --argtook the base64 archive as a command-line argument — 163,840 characters for a 20-file app — and died withjq: Argument list too long. Separately itstarexclude list had no.venv, so the same app produced a 58 MB build context instead of 60 KB. - A security claim was false. The skills stated
/agent/*"is never reachable from the public URL". Skippingapi_routesremoves the gateway, not the network:<slug>.apps.manaurum.comis Traefik straight to the container. Verified against a live deploy on 2026-07-26 — an unauthenticated POST reaches the handler. An app written to that sentence ships an open endpoint.
Added
templates/v2-starter/is now shaped like a real app.src/auth.py(RS256user_contextverification) andsrc/capability.py(the gateway client) as shared infrastructure;src/main.pyandsrc/agent_routes.pyas the two surfaces built on them. Apps grow by adding surfaces, not by growing one file — and the starter now demonstrates that instead of asserting it.- Two working
agent_capabilities, manifest entry through to handler. This is the MAN-1396 half: the handler side was documented nowhere, so the identity trap was invisible.read_my_notetakes no input on purpose — a capability with auser_idargument lets the model read somebody else's data by passing a different one. - A test suite that runs offline —
tests/conftest.pygenerates a throwaway RSA keypair and signs its ownuser_contexttokens, so JWT verification and the agent handlers are testable with no database, no account and no network. 19 tests, including every way a token can be wrong and one that fails if a user can read another's note. Testing had zero occurrences in the plugin before this release. tests/test_routes.pycovers the wiring, not just the pieces. Unit-testing the verifier and unit-testing a handler both stay green when the two stop being wired together — and the route is then open on a public hostname. So these drive real HTTP through the app withTestClient(no database, no new dependency:httpxis already a runtime dep). Three mutations that a 13-test suite waved through now go red: makingnote_key()return a constant, droppingDepends(auth_claims)from an/agent/*handler, and dropping it from a route inmain.py. Added in review — a starter whose green suite implies its security-critical lines are covered teaches the wrong lesson exactly where this release claims to teach the right one.skills/manaurum-app/references/reference-apps.md(MAN-1394) — the reference ladder.shift-checklist(22 files) as the one to read whole,family-space-v2(77 files) as the ceiling,libias the testing exemplar, each with the load-bearing excerpt inlined so the page stands alone for a developer without the monorepo. Named paths are provenance, not the deliverable.- A testing section in
manaurum-setup/SKILL.md, leading withpytestrather thandocker build, and explaining why the local401 missing_user_contexton/api/meis the correct answer rather than a failure.
Changed
manifest_v2.json→manifest.jsonin all 14 places across the three skills. The one occurrence left in this file is history and stays.deploy.shand the two quickstart snippets switched tojq --rawfile/--slurpfile(reads the payload from disk, noARG_MAXceiling) and gained.venv venv __pycache__ .pytest_cache dist buildin thetarexcludes. Fixed at all four sites, then run verbatim against a real project to prove it.agent_capabilities[]inreferences/v2-platform.md— the three-field stub is replaced by a full entry (description with a positive trigger, an ordering constraint and a negative), the handler excerpt, and the note that a validuser_contextJWT is authentication, not authorization.is_writeis documented as declarative only. The runtime does not read it for hosted apps: there is no such column, the deploy-time sync ignores the key, and at request timedispatch == "backend"forcesis_write=Truefor every capability, readers included. So read-only capabilities take the write path and are excluded from cross-app insight, which filters onnot is_write. Tracked as MAN-1425 — the skill now says what is true rather than what was intended.manaurum-setup/SKILL.mdstarts from the working starter instead of assembling an app from snippets, and says explicitly that where a snippet disagrees withtemplates/v2-starter/, the starter wins. Its inlineindex.htmlbody was deleted in favour of pointing at the starter's; the lesson about the handshake stayed.- Manifest examples use
"port": 8000(matching the starter and every hosted app in production) instead of 80, and carry anagent_capabilitiesentry. - The starter no longer draws a
migrations/directory it does not ship. Git cannot track an empty directory, and the obvious fix is a trap:migrations/is SQL-only, so a.gitkeepsitting in it raisesBundleMigrationError: non-SQL file in migrations/and fails the deploy — breaking the starter's one promise, that it deploys green as-is. The README says so instead, and § Storage already covered when to create the directory. README.md— the "three rules" table gained the/agent/*one; the quick start copies the starter instead of runningmanaurum app init, and says why; and the claim that the starter is "byte-identical tomanaurum app initoutput" is retracted, because it is not.
Not changed
templates/legacy-v1/— untouched, still there for apps already on v1.templates/v2-starter/was not deleted. Removing it in favour ofmanaurum app initis MAN-1393's item 4. The CLI-side rewrite exists (MAN-1397, monorepo PR #1455, in review as this ships) and the two scaffolds converged on the same shape independently — but that rewrite is in no released wheel, andpip install manaurum-clistill 404s on PyPI (MAN-1385), so the only CLI a developer can install iscli-v0.2.0, built before it. Deleting the starter now leaves them with no working scaffold at all. Sequencing: #1455 merges → a CLI release ships → the quick start repoints and this directory goes. Deferred, not dropped.marketplace.jsoncarries no version field and did not get one.
2.5.0 — the human-facing half catches up (MAN-1365)
Why
2.4.0 fixed skills/** for v2 and stopped there. The two surfaces a person reads
first were untouched, so the agent read correct v2 while the human read a v1 pitch:
README.mddescribed apps as "regular web pages in an iframe", sold the Test Harness and the XP theme, offered "paste HTML or upload ZIP" and Vercel/Netlify hosting, and documented a "Private → Unlisted → Public App Store" ladder that does not exist. It also announced itself as version 1.6.0 while the plugin shipped 2.4.0.templates/held three v1 artifacts and no v2 starter at all, so every app regenerated container boilerplate from prose.
What changed
- README rewritten for v2. What a v2 app actually is (a container on
<slug>.apps.manaurum.com), the capability gateway and the signed user-context header in a paragraph each,visibility.modeinstead of the invented ladder, the three skills and when each fires, and a copy-paste quick start. The three rules that cost first-timers the most time —/api/*default-deny,runtime.port(neverEXPOSE), and the 10-secondmanaurum:readyhandshake — are a table near the top rather than buried in a skill. templates/v2-starter/— byte-identical tomanaurum app initoutput. It deploys unchanged: serves a UI that answers the shell handshake, verifies a real RS256 user-context JWT on/api/me, and does a key-value round trip through the capability gateway on/api/notes. Regenerate it with that command rather than editing it here, so the two distribution channels cannot drift.templates/legacy-v1/— the old iframe artifacts, kept only for apps already on v1.- The CLI is installable again.
pip install manaurum-cliis advertised in six places across the product but the package has never existed on PyPI. Until it does, the wheel ships as a release on this repo (cli-v0.2.0) and the README points at it. Verified end to end in a clean virtualenv: install →manaurum app init→app validate. - An "honest gaps" section. No local dev loop, one-line build failures, "succeeded" meaning built-and-scheduled rather than serving, no cron or webhooks behind the manifest fields that exist for them, logs without follow, and subdomains being public knowledge through Certificate Transparency the moment an app first deploys.
Not changed
skills/** — corrected in 2.4.0 (MAN-1330) and re-read during this work; no new factual
errors found.
2.4.0 — realignment with the monorepo (MAN-1330)
Why
The skills documented several mechanisms that do not exist in the platform, so an app authored strictly from this plugin could not work:
- Its API 404s.
runtime.api_routeswas never mentioned anywhere in the plugin. The gateway is default-deny on/api/*: an undeclared path returns404 route_not_declaredand never reaches the container, so the app looks like it has a backend bug with silent logs. - Its container 502s. The plugin taught "the platform reads your
EXPOSEline and routes Traefik to it". Nothing in Core parsesEXPOSE. The upstream is<swarm-service>:<port>whereportismanifest.runtime.port, default 80 — so the Node/FastAPI Dockerfiles we shipped (EXPOSE 8080/EXPOSE 8000, noruntime.port) produced a green deploy that 502s on every request. - It is unusable as a desktop window.
manaurum:readywas never taught for v2 at all. The shell hard-enforces the handshake for both runtimes (READY_TIMEOUT_MS = 10_000) and covers the app with "App is not responding" when it is missed — and the standalone<slug>.apps.manaurum.comURL works fine without it, so the omission is invisible until someone opens the app on the desktop. This is not hypothetical: MAN-1321 shipped exactly that bug in the first-party app Libi.
On top of that, the deploy flow was taught as synchronous ({"status": "succeeded"} from
the POST) when it is 202-plus-poll, and the runtime credential was taught as a
developer-token env var (MANAURUM_V2_TOKEN) that the platform has never injected.
permissions[] is correct and was deliberately kept. An audit during this work flagged
the permissions[] documentation added in 2.3.0 as an error; that flag was itself wrong.
MAN-1316 added permissions to manifest_v2.schema.json (enum ["microphone"], drives the
iframe allow= Permissions-Policy delegation) and 2.3.0 documents it accurately. Do not
"re-fix" it.
Fixed — mechanisms that did not exist
EXPOSE→runtime.port. Removed the "platform reads yourEXPOSE" claim frommanaurum-app/SKILL.mdandmanaurum-setup/SKILL.md.EXPOSEis documentation only;runtime.port(default 80) is the sole input, and the three numbers that must agree areruntime.port, yourCMD's port, andEXPOSE. Added the127.0.0.1-vs-0.0.0.0trap, and made the starter Dockerfiles declare a matchingruntime.port.MANAURUM_V2_TOKEN→MANAURUM_RUNTIME_TOKEN+MANAURUM_CORE_URL. The container never carries a developer token: the platform injects a per-(tenant, app)mna_*runtime credential, minted fresh on every deploy. The call contract incapabilities-reference.mdand the worked examples inmanaurum-app/SKILL.mdandmanaurum-setup/SKILL.mdnow build the URL from${MANAURUM_CORE_URL}and authenticate with${MANAURUM_RUNTIME_TOKEN}.runtime.env_secretsdeleted — it is not in the schema and Core never reads it. It appeared to work only because theruntimesub-object is not strict, so it validated and did nothing.MANAURUM_BROKER_URLdeleted — never injected (MAN-163 removed it because the shared broker DSN had grants on every app's schema). Every recipe built on it is gone.migrate_commandis documented as dead. It is in the schema, but Core has no call site for it — an app whose schema depends on it deploys green with no tables. The migration path ismigrations/*.sql, run once per (app, tenant).- Deploy is asynchronous.
POST /api/dev/v2/deployalways returns 202 with{"deploy_job_id", "status": "pending"}— neversucceeded. Replaced the "sync response, ~7–10s" text inmanaurum-app/SKILL.mdandmanaurum-deploy/SKILL.md, and rewrote thedeploy.shtemplate around a real polling loop. Added: only401/403 app_id_out_of_scope/422 invalid_archive_b64fail synchronously; manifest, migration and Docker failures surface asstatus: "failed"on the job. succeeded≠ serving. There is no readiness probe in the hosted path, so a crash-looping or wrong-port container still produces a green job. Every deploy path now ends in an explicit/healthzcheck.runtime.byo_endpoint_url→runtime.entrypoint. The old spelling appears nowhere in Core and (non-strict sub-object again) validates cleanly while leaving a BYO app with no URL.- Root
descriptionremoved from the v2 example manifest inv2-platform.md— the v2 root isadditionalProperties: false, so it is a hard rejection. It belongs inmetadata.description; likewiseicon→frontend.icon,category→metadata.category. os.tenant_config.getre-documented against the handler. It does not readtenants.featuresor install-timetenant_config; it readstenants.app_builder_configthrough a Pydantic model with one field (prompt_extension) andextra: "ignore", so every other key returnsnullandapp_idis ignored. Flagged as unreliable.- v1 status codes corrected in
publishing.md: version reuse is409 rejected_version_conflict(not 400); an over-50 MB bundle is413 rejected_bundle_too_large.
Added
runtime.api_routes— a full section inmanaurum-app/SKILL.md, the field reference inv2-platform.md§ 2, the scaffold inmanaurum-setup/SKILL.md, theapp.fetchnote insdk-api.md, and a triage row inmanaurum-deploy/SKILL.md. Covers default-deny,auth: "user"vs"anonymous", the 60suser_contextJWT injected asX-Manaurum-User-Context,streaming: true, precedence, that there is nomethodfield, and that/api/x/*does not match the bare/api/x.- The
manaurum:readyhandshake. New "Step 2.5 (MANDATORY)" inmanaurum-app/SKILL.md, a full contract section insdk-api.md(the realmanaurum:initpayload, the 10s timeout, the three origin/source/type checks the shell applies, the belt-and-braces inline-listener + post-mount pattern that shipped for Libi in MAN-1321), and the listener baked into the starterindex.htmlinmanaurum-setup/SKILL.md. sdk-api.mdnow covers v2. New runtime-selector table at the top, a "Platform v2 — frontend SDK (manaurum-v2.mjs)" section (init(),onReady/onThemeChange/onDeviceChange/onAuthFailure, context getters,app.fetchwith its opt-inretrysemantics,app.pickFromDrive(), and what the SDK deliberately does not do), theV2_ALLOWED_MESSAGESframing list, and the v1 bridge verbs a v2 frame is refused. Everything below the new "Legacy v1" divider is explicitly marked v1-only.- Migrations documented end-to-end (
v2-platform.md§ 7, plus summaries in the setup and deploy skills):migrations/*.sql, flat and SQL-only, run once per (app, tenant) in lexical order, sha256-pinned; and the DDL validator's four classes —additive/neutralpass,destructiveneedsmigration.breaking: true,forbidden(DO $$,COPY,CREATE EXTENSION,BEGIN/COMMIT, anySET, role/database DDL) is never allowed — with default-deny as the master rule. Includes the context-sensitive additives (CREATE INDEX/SET NOT NULLon a fresh object) and the real-worldDO $$rejection that hit Libi (MAN-1327). - Runtime DB reality:
DATABASE_URLis a per-(app, tenant)appusr_*login,NOSUPERUSER NOBYPASSRLS, no CREATE — soCREATE TABLE IF NOT EXISTSon boot dies withpermission denied for schema app_<slug>__<hex>. PlusMANAURUM_TARGET_SCHEMAandCORE_USER_CONTEXT_PUBLIC_KEY_PEMin every env-var table. datamodes.{"none": true}for an app with no Postgres of its own — omitting the block selects managed mode and provisions a schema + role. Added to the setup scaffold and both manifest references.- The rest of the v2 root surface in
v2-platform.md: the complete 23-key list plusplatforms,provides,consumes,optional_capabilities,offline,tenant_config, andagent_capabilities[](with the server-to-serverPOST /agent/<name>dispatch, which bypassesruntime.api_routes).webhooksandschedulesare marked shape-validated only — Core does not invoke them in v2.x. os.calendar.list_events/os.calendar.create_eventincapabilities-reference.md(idempotent upsert viasource_ref, overlap-not-containment range semantics, no pagination), and a pre-dispatch gate table coveringcapability_not_granted,tenant_mismatch,user_context_required,invalid_user_context,capability_denied_in_dev_mode. Grant enforcement is unconditional — an install with an empty grant list denies everything, so adding a capability and redeploying is not sufficient.- A "What will bite you" section in
manaurum-app/SKILL.md, for the failures that only appear inside the desktop: noalert()/confirm()/prompt()(the sandbox isallow-scripts allow-forms allow-same-origin;allow-modalsis never emitted), Core force-assignsframe-ancestorsand stripsX-Frame-Optionson/apps/*(but leaves the rest of your CSP), a relativefrontend.iconpaints as literal text, unknownruntimekeys validate and are ignored, and.env*is not excluded by the CLI packager. publishing.mdrewritten around a v2 section: publish-vs-deploy (App Builder validates the manifest synchronously with422; the CLI validates it inside the job), the poll surfaces,experiment.platform_v2_hosted_runtime, the three icon rules including the Dev Hub route's 8-character limit, and the listing-edit/manifest overwrite trap. The v1 tenant catalog is retained below, demoted and labelled legacy.manaurum-deploy/SKILL.md: the NDJSON/streamprogress endpoint,version_labelas a required rollback argument (and rollback being async too), the per-(tenant, app) bare-git history behindfetch-source— with the warning that a secret in the tar is permanent even after the tarball window prunes — and a "failures you'll actually hit" table keyed by symptom.manifest-spec.mdv1-only banner with a v1→v2 field-mapping table, and an explicit note thatpermissionsexists in both versions and means different things.- A maintenance note in
capabilities-reference.md: the registry underbackend/app/services/capabilities/is the source of truth, the checklist isdocs/standards/ADDING_A_V2_CAPABILITY.md§ 9, and there is no automated parity check between the code and this plugin.
Changed
egress_allowed_hostsnow documents the enforcement point (copied onto the version row, read by theos.http.fetchhandler) and flags the live monorepo bug where declared hosts are written into the container's/etc/hostsas0.0.0.0 <host>— the inverse of an allow-list. Guidance: route all external HTTP throughos.http.fetchand do not build on either reading of raw container egress until it is resolved.- The
runtimesub-object's non-strictness is described, not advocated. It is whyport/egress_allowed_hostswork at all and why"prot": 8000deploys green and 502s. Whether it should be strict is called out as an open question, not a recommendation. - Redeploying the same
(app_id, version)is no longer described as a DB no-op — the pipeline inserts anotherv2_app_versionsrow every time; it is idempotent only for the running service. - Scaffold layout:
src/plus a narrowCOPY src/, a starter.dockerignore, and.env.manaurumdocumented as deploy-time-only.manaurum-appandmanaurum-setupname that credentialMANAURUM_TOKEN;manaurum-deploy/SKILL.mdstill spells the same deploy-time variableMANAURUM_V2_TOKEN, which is cosmetic but not yet unified.
2.3.0 — 2026-07-19
- Voice-app platform surfaces (MAN-1316, docs work item MAN-1323) —
the skill can now build a working voice app end-to-end:
os.ai.transcribedocumented (capabilities-reference + the SKILL.md capability table): BYOK speech-to-text on the tenant's OpenAI key, ≤ 25 MB decoded audio, default modelgpt-4o-transcribe, real error codes verified against the handlers (invalid_audio_base64,audio_too_large, 412integration_not_configured, and the fact that every upstream failure is 502upstream_error:openai— never 504).- Manifest
permissions[](browser Permissions-Policy delegation, enum["microphone"]) added to the manaurum-setup scaffold + validation rules, the manaurum-app manifest steps, and the v2-platform.md § 1 field reference — a scaffolded mic app no longer ships broken inside the shell iframe. os.http.fetchsection REWRITTEN against the actual handler: the old text taught a text-onlybody(which corrupts binary payloads) and a nonexistenttimeout_secondsfield. Now documentsbody_base64/response_format: "base64"(~5 MB each way),timeout_ms, the real output shape (content_length,elapsed_ms), and the real error codes (unsafe_url,host_not_in_allow_list,upstream_response_too_large, …).
- Version note: the 2.2.0 changelog entry below shipped on 2026-06-24 but
plugin.jsonwas never bumped past 2.1.0; this release corrects the drift by moving straight to 2.3.0.
2.2.0 — 2026-06-24
- Source retention (MAN-990 / MAN-993): the deploy skill now documents
that the platform retains each version's build context (your uploaded tar)
in object storage instead of discarding it — your source is no longer
single-copy on your machine, and a version stays rebuildable after its
image is pruned. Added the
GET /apps/{app_id}/versions/{version}/sourcesigned-download route, thehas_sourceflag on the versions list, the rolling-window retention policy, and themanaurum app fetch-source/ DevHub "Download source" surfaces.
2.1.0 — 2026-06-10
- Drive bridge (MAN-608): documented the
os.drive.*capability family (stage/publish "Save to Files", list/read/write in granted folders), thedrive.{slug}.file.*change events, and theapp.pickFromDrive()SDK helper (manaurum-v2.mjs 2.1.0). Reframedos.files.*as per-app private scratch + documented the newos.files.list.
Changelog
2.0.0 (2026-05-07) — Platform v2 is the default flow
This is a major release. The skill defaults flip: every new app is now scaffolded, taught, and deployed as a Platform v2 containerized hosted app. The v1 (iframe + manaurum.js + mnu_* token + /api/dev/apps/deploy) flow is preserved as a legacy section in each skill, only used when an existing v1 app needs maintenance.
Why
Platform v2 shipped to production on 2026-05-06/07. New apps have access to the capability gateway (KV / files / AI / OCR / notifications / events / RPC / HTTP egress / audit), per-tenant isolation via FORCE-RLS on every Core table, and a one-command deploy that yields https://<slug>.apps.manaurum.com with TLS in ~7–10 seconds. There is no Core PR for any of this. v1 cannot match those primitives — every v1 app is a static iframe with permission-gated postMessage calls, and per-tenant deploys are independent.
The team's working assumption from now on: all new app work goes on v2. v1 is feature-frozen for existing apps. This skill release reflects that.
Added
manaurum-app/SKILL.mdrewritten with v2 as the primary flow. Teaches: container model, env vars, capability gateway contract, manifest v2 minimum, common rejection codes, what NOT to do. Legacy v1 path preserved as a brief section at the bottom with pointers to the v1 references.manaurum-deploy/SKILL.mdrewritten. v2 flow first (POST /api/dev/v2/deploy, build context as base64 tarball, sync response shape, rollback, version listing). Legacy v1 deploy preserved.manaurum-setup/SKILL.mdrewritten. v2 project scaffolding first (Dockerfile+manifest_v2.json+.env.manaurumwithmna_*token). v1 scaffolding preserved.references/v2-platform.md— long-form companion. Manifest field reference, runtime modes, capability contract, token issuance/revocation, deploy lifecycle (build → push → swarm → traefik), rollback, migrations + dedicated app schemas, visibility + App Store v2.references/capabilities-reference.md— input/output reference for every capability shipped in v2:os.kv.*,os.tenant_config.get,os.secrets.*,os.files.*,os.ai.*,os.ocr.extract,os.notifications.send_to_user,os.events.emit,os.http.fetch,os.compliance.audit_query,os.apps.call,os.apps.bulk_export.
Changed
- Plugin
descriptionupdated to mention v2-as-default + legacy v1 support. - Banner added at the top of all three SKILL files explaining "v2 is the new default" and how to decide between v2 and v1 for a given task.
Preserved (no behavior change)
references/manifest-spec.md— v1 manifest schema reference. Still authoritative for v1 apps.references/sdk-api.md— v1 SDK API (storage.*,files.*,db.*,ai.*,mul.*, etc.). Still authoritative for v1 apps.references/design.md— Smoothie + XP themes for v1 iframe apps.references/publishing.md— App Store v1 submission flow.
Tokens — mna_* vs mnu_* vs mdev_*
| Format | What it's for | Endpoint |
|---|---|---|
mna_* | v2 default. Capability gateway + hosted-runtime deploy. | /api/capability/<name>, /api/dev/v2/deploy. |
mnu_* | Legacy v1 deploy. | /api/dev/apps/deploy. |
mdev_* | Legacy App Builder (deprecated; migrated to mna_* 2026-05-07). | removed. |
The three are NOT interchangeable; using one against the other's endpoint returns 401.
Migration path for skill consumers
If you have a Claude Code instance with this plugin installed at v1.15 and you upgrade to v2.0:
- Existing v1 apps continue to work — v1 deploy endpoints + tokens are unchanged on the platform side.
- New
/manaurum-app,/manaurum-deploy,/manaurum-setupinvocations now teach v2 by default. To explicitly target v1, ask: "scaffold a v1 (legacy iframe) app". - The
manaurum.jsSDK is unchanged. Static URLhttps://manaurum.com/sdk/manaurum.jscontinues to serve.
Reference
- Manaurum PRs that shipped v2 to prod: #418 (capability gateway core), #420 (
os.files.*), #422 (os.tenant_config+os.secrets), #425 (os.ai.*), #429 (os.ocr.*), #430 (os.events.emit), #431 (os.compliance.audit_query), #432 (os.apps.call), #439 (R-4 hosted runtime backbone), #450 (DevHubmna_*token UI), #458 (R-4 production wiring — registry + swarm + traefik), and hot-fixes #451, #453, #454, #455, #456, #459. - First v2-deployed app on prod:
https://v2-smoke.apps.manaurum.com(2026-05-07, deployed viaPOST /api/dev/v2/deployfrom cold start in ~8s).
1.15.0 (2026-04-30) — F1.5 evolution — renamed_from + dedicated include
Added
renamed_fromfield-level hint documented inmanifest-spec.md. Set"renamed_from": "<old_name>"on a dedicated field and the diff engine emitsALTER TABLE RENAME COLUMNinstead of the default DROP+ADD on next deploy. Additive — no data loss. Drop the hint on the deploy after the rename. Validator R9 rejects shared-only use, self-rename, and source-name-still-exists collisions.- R9 row in the cross-field rules table.
includefor dedicated entities documented insdk-api.md. The shared-storageincludehad a convention-based FK lookup (child must have<parent>_idfield); dedicated uses the explicitreferencesdeclaration. Single indexedIN(...)query per child type — no N+1. Caps unchanged: 4 includes max, 100 children per parent.
Notes
- Pure-documentation release. Backend changes shipped in Manaurum PR #341 (merged + deployed 2026-04-30). Runtime API and SDK build unchanged — same
app.db.list('parent', { include: [...] })works against either tier.
1.14.0 (2026-04-30) — F1.5 hardening — R8 quotas + destructive add-NOT-NULL
Added
- R8 row in the cross-field rules table (
manifest-spec.md). Per-app quotas now enforced by the validator: max 50 entities per app, max 100 fields per entity, max 20 compound indexes per entity. Generous; you should not hit these in a real app — the point is to surface a clear early reject if a manifest is accidentally ballooning (codegen bug, abuse).
Changed
- Additive vs destructive table in
manifest-spec.md— adding arequired: truefield to an existing entity is now classified as destructive by the diff engine. Previously it would slip through as additive and PG would reject the ALTER on populated tables with a generic error. Now the deploy returns a cleanrejected_destructive_changewith a description pointing at the safe two-step pattern (add as optional → backfill → tighten to required), or the dev passesallow_destructive=trueto make the intent explicit.
Notes
- Pure-documentation release — runtime API and SDK build unchanged. Companion to Manaurum PR #339 (validator + diff engine + telemetry).
1.13.0 (2026-04-30) — graduated storage (storage: "dedicated")
Added
- Dedicated storage tier documented. New "Dedicated storage" section in
manaurum-app/references/manifest-spec.md: when to use it (>10k rows / per-tenantUNIQUE/ FKs / compound indexes), full example, the field-level extras (unique,references), the entity-levelindexes[]array, the R1–R7 cross-field rules, additive vs destructive change classification, and the "runtime is the same" reminder. - SKILL.md updated so the Database quick overview surfaces both tiers (shared = EAV-pivot, default; dedicated = real PG table). Validation rules table updated —
entities[].storageis no longer marked "onlyshared". The sameapp.db.create / get / list / update / deleteworks against either tier; the platform routes behind the unchanged interface.
Why this matters
Until now storage: "dedicated" was reserved-but-rejected. Apps that grew past EAV-comfortable size had to either accept slow EAV reads or ask the platform team for an Alembic migration + Core PR. With the F1.5 graduated-storage path live (Manaurum PR #328 merged + deployed on prod 2026-04-30), an external developer writes one word in the manifest and gets a real table — real columns, real indexes, real FKs, real UNIQUE — auto-generated and migrated by the deploy pipeline. The boundary "go to Core via PR" moves from "I need one JOIN or index" up to "I need shell-level intervention".
Notes
- Pure-documentation release — no template change. The runtime API and SDK build are unchanged.
- Storage tier is a one-way decision per entity. Plan before first deploy: changing
storagebetweensharedanddedicatedafter deploy is rejected as a destructive transition. - (Plumbing only: 1.12.0 shipped the Component Library docs but missed the
plugin.jsonversion bump — this release lands at 1.13.0 to keep the cache directory layout monotonic.)
1.12.0 (2026-04-30) — manaurumOS Component Library
Added
manaurum.mul.*documented end-to-end. New "Component Library (MUL)" section inmanaurum-app/references/sdk-api.mdcoversmul.list(),mul.search(query, filters?),mul.get(id)— thin same-origin wrappers over the public read-only/api/library/*endpoints. Includes wire format, build-time vs runtime guidance, and the "no permission required" note (the library is curated and unauthenticated).SKILL.mdquick overview updated to surface the library as a first-class building block. Step 2 (design) now nudges devs to browse the catalogue before drawing from scratch.design.mdopens with a "don't design from scratch when you can borrow" pointer to the library.
Notes
- Underlying surface ships in PRs #325 (HTTP API + catalogue UI at
/library, merged), #326 (SDK v1.9.0 helpers, merged), #327 (App Builder catalogue injection underexperiment.app_builder_uses_library, merged). - The library is curated, public, and read-only. No tenant scoping, no auth headers — same-origin fetch is enough. Iframe apps with strict CSP
connect-srcshould bake chosen components into the bundle at build time rather than fetching at runtime. - Pure-documentation release — no template change.
1.11.0 (2026-04-28) — db.batch (atomic multi-write)
Added
manaurum.db.batch(ops)(Phase 3 slice 3.1). Run multiple writes in one transaction — all-or-nothing.opsis an array of up to 50 entries, each{op: 'create'|'update'|'delete', entity_type, record_id?, data?}.- Single tenant-bound DB session, single
commit()at the end. Any failure rolls the whole batch back. - Errors include
at: <index>so the app can point at the failing op precisely; status code matches the underlying single-op error (400 / 404 / 405 / 422). - SDK build: v1.8.0.
- Documented in
references/sdk-api.md→ "Database API" →db.batchwith op-shape table, atomicity model, error shape, and wire format.
Use cases
Receptions Confirm (status flip + N stock_movement inserts), bulk import, multi-step status transitions, anything where a partial commit would corrupt an app-level invariant.
Notes
- Forward-additive — existing single-op SDK calls are unchanged.
- Larger workloads must chunk client-side; chunks are atomic individually but not collectively.
1.10.0 (2026-04-28) — db.list child-fetch via include
Added
db.listincludeoption (Phase 2 slice 2.4). Hydrate each parent record with its children in one round-trip:include: ['<child_entity>', ...]— array of distinct child entity names, max 4 per call.- Convention-based FK: the child entity must declare
<parent_entity>_idUUID withindexed: truein its manifest. - Up to 100 children per parent (sorted by
created_atasc); extras dropped silently for v1. - Implementation is N+1 (one child query per parent per include); promote to JOIN once we have planner data.
- Nested includes are not supported — hydrated child records always have
includes: null. - SDK build: v1.7.0.
- Documented in
references/sdk-api.md→ "Database API" with example, rules, and new errors (InvalidIncludeError422,include_must_be_json/include_must_be_array400).
Notes
- Forward-additive —
db.listcalls withoutincludekeep working unchanged. - Phase 2 of the SDK roadmap is now fully shipped: 2.1 (db.list operators) + 2.2 (entity immutability) + 2.3 (db.aggregate) + 2.4 (child-fetch).
1.9.0 (2026-04-28) — db.aggregate
Added
manaurum.db.aggregate(entity, options)(Phase 2 slice 2.3). Single-round-trip GROUP BY for dashboards.metrics: list (max 8) ofCOUNT(*)/SUM(<field>)/AVG(<field>). Numeric metric fields must beindexed: trueandinteger/decimal.group_by: anyindexed: truefield.where: same operator grammar asdb.list(slice 2.1).- Hard cap of 1000 distinct groups —
422 AggregateCardinalityExceededon overflow. - Decimal metric values come back as JSON strings (Decimal-safe); UUID/timestamp keys also stringified. SDK build: v1.6.0.
- Documented in
references/sdk-api.md→ "Database API" with response shape, error table, and wire format. Errors:InvalidMetricError,AggregateCardinalityExceeded,metrics_must_be_json,metrics_must_be_array.
Notes
- Forward-additive — existing
db.list/db.create/ etc. unchanged. MIN/MAXandCOUNT(field)deferred. Once we have planner data on real datasets, MIN/MAX are the next likely additions.
1.8.0 (2026-04-28) — db.list filter operators + entity immutability flags
Added
- Range / IN filters on
db.list(Phase 2 slice 2.1). Thewhereoption inmanaurum.db.list(entity, { where })now accepts structured operators on indexed fields:- Scalar value = equality (back-compat, e.g.
{ status: 'open' }). - Operator dict =
{ op: value, ... }with operatorseq,gt,gte,lt,lte,in. Multiple ops on one field share a single JOIN, so{ created: { gte: '2026-04-01', lt: '2026-05-01' } }runs as one range predicate. intakes a non-empty list (max 100 items).- Filtered fields must still be
indexed: true— same rule assort_by. - Wire format:
GET /api/app-data/{slug}/{entity}?where=<URL-encoded JSON>— the SDK and bridge handle the encoding for you. - New error codes:
422 FilterOperatorError,422 IndexValueCoercionError,400 where_must_be_json,400 where_must_be_object. All documented inreferences/sdk-api.md→ "Errors".
- Scalar value = equality (back-compat, e.g.
- Entity immutability flags (Phase 2 slice 2.2). Manifest entities can declare append-only / non-deletable semantics enforced at the storage layer:
"immutable": true— every UPDATE on records of this entity is rejected with405 EntityImmutable."no_soft_delete": true— every soft-delete is rejected with405 EntityNotSoftDeletable.- Both default to
false; combine them for a strict append-only journal (e.g. Receptionsstock_movement). - Documented in
references/manifest-spec.md→ "Entities" with astock_movementexample.
Notes
- Both changes are forward-additive. Existing manifests and
db.listcallers keep working unchanged. db.listwith operators: the SDK build is v1.5.0 (bump from v1.4.0). The platform's bundled SDK is updated automatically on deploy; tenant apps can import either version.
1.7.0 (2026-04-28) — runtime AI API
Added
ai.usemanifest permission. New entry in the v1 permissions enum (manifest-spec.md→ "Permissions enum (v1)"). Declare it if your app callsmanaurum.ai.completeor.vision. v1 runtime doesn't enforce it (yet) — declaration is for transparency at install time and forward compatibility when per-tier limits arrive. Workspace admin's gate stays at Settings → Agents (mode='disabled'→AI_DISABLED).manaurum.ai.*runtime API documented end-to-end. New "AI API" section inreferences/sdk-api.mdcovers:app.ai.complete({ prompt, system? })— text completion.app.ai.vision({ prompt, image, system? })— image+prompt completion.imageaccepts{file_id}(resolved server-side from the app'sstored_files) or{data_url}(inline base64).- Wire format:
manaurum:ai-complete/manaurum:ai-visionpostMessage verbs →POST /api/app-ai/{slug}/completeand/vision. - Error codes:
AI_NOT_CONFIGURED,AI_DISABLED,VISION_UNSUPPORTED,IMAGE_INVALID,IMAGE_MIME_UNSUPPORTED,NOT_FOUND,TIMEOUT (90s). - Vision provider support in v1: openai (gpt-4o family), openrouter, anthropic (claude-3 family), deepseek, glm. Gemini rejects with
VISION_UNSUPPORTED.
SKILL.mdquick-overview updated to surfaceapp.ai.*as a first-class capability alongsidedb.*.
Notes
- The iframe never sees the LLM API key. The platform resolves the workspace's configured provider+model from Settings → Agents and writes per-app
llm_token_usagerows attributed to the callingapplication_idso workspace admins see per-app spend. - No manifest permission required in v1; the gate lives in Settings → Agents (a workspace admin can disable AI for a specific app, surfacing as
AI_DISABLED). A formalai.usemanifest permission is on the roadmap and will be additive.
1.6.0 (2026-04-27) — runtime Database API
Added
manaurum.db.*runtime API documented end-to-end. New "Database API" section inreferences/sdk-api.mdcoverscreate,get,list(with pagination + indexed sort),update(full replace), anddelete(soft). Includes wire format (postMessage type → HTTP route), error table mapping422 EntityTypeNotDeclared,404 record_not_found,422 FieldNotIndexedError, etc.- Manifest ↔ runtime bridge documented in
references/manifest-spec.md. Explains that declaringentities[]at deploy time is what enablesmanaurum.db.*calls at runtime, with a worked example showing why undeclared types and unindexed sort fields fail. SKILL.mdquick-overview updated to makedb.*the first-class persistence path;storage.*/files.*/collections.*demoted to a single "legacy runtime APIs" line.
Changed
- The "Quick overview (v1.5 SDK)" bullet pair in
manaurum-app/SKILL.mdnow leads with the manifest-gateddb.*API.
Note
This release is purely documentation — the underlying runtime has been live since W4.3 (backend/app/routes/app_data.py + the manaurum.db.* block in frontend/public/sdk/manaurum.js). No backend or SDK shipping change.
1.5.0 (2026-04-27) — BREAKING: tenant-aware Deploy API
Changed (BREAKING)
manaurum-deployrewritten for the new Deploy API. The legacy/api/developer/apps/.../hosting/pasteflow (paste-HTML,mdev_*tokens) is no longer documented. Tenant developers now go through:POST /api/developer/tenant-tokensto mint a tenant-scopedmnu_*token.POST /api/dev/apps/deploywith{manifest, bundle (base64 zip)}.
MANAURUM_TOKENenv var renamed toMANAURUM_TENANT_TOKENin templates and deploy script. Old name is gone — update local.env.manaurumfiles.- Manifest schema replaced with v1 (frozen). The legacy shape (
runtime.entrypointURL,runtime.sandbox,description,compatibility.min_shell_version, permissions liketheme.read/storage.*/files.*/window.manage) is no longer accepted by the deploy validator. The new schema requiresmanifest_version: "1",manaurum_sdk_version: "1",slug,version(semver),entry_point(bundle-relative path), and limits permissions to a 7-value enum (auth.read_user,auth.read_workspace_members,navigation.open_app,navigation.close_self,events.subscribe,db.read_own_entities,db.write_own_entities). manaurum-apprewritten for multi-tenant context. The skill now teaches thatmanaurum:initcarries atenantblock ({id, slug}) plusworkspace,user,appblocks — apps can render tenant-aware UI and identify their B2B operator.templates/manifest.jsonandmanaurum-setupscaffolding updated to v1 schema +MANAURUM_TENANT_TOKEN.
Added
- Manifest v1 reference with the full enum of permissions, entity field types, integration declarations, and rejection codes.
- New deploy rejection-code table with one-line remediations for every
rejected_*code returned by the Deploy API. - Tenant context bridge documentation:
payload.tenant.slugfor B2B kustomization, with explicit "do NOT use as a security filter — RLS already enforces" warning. - Per-tenant deploy guidance: a
mnu_*token is bound to ONE tenant; multi-tenant apps require independent deploys with separate tokens.
Removed (from skill docs)
- Legacy
/api/developer/apps/quick-create,/hosting/paste,/hosting/upload,/manifest,/probe-entrypoint,/diagnosticsendpoints. They still exist on the platform for the in-platform App Builder UI but are no longer the recommended path for external developers. mdev_*token references.- Permissions outside the v1 enum (
theme.read,storage.*,files.*,window.manage,notifications.*,tasks.suggest) from the manifest validation table. Runtime SDK methods may still work but are not gated by manifest in v1 — treated as evolving.
Migration
If you have an existing app deployed via the legacy flow:
- Generate a new
mnu_*token (POST /api/developer/tenant-tokenswith your session JWT). - Convert your manifest to v1 schema (see
manaurum-app/references/manifest-spec.md). - Bundle as
bundle.zipwithindex.htmlat the root. - Redeploy via
POST /api/dev/apps/deploy. The new deploy creates a freshapplicationsrow in your tenant's catalog under the v1 schema.
1.1.0 (2026-04-08)
Added
- UI Kit reference: comprehensive design system with exact styles from built-in apps — cards, buttons, inputs, labels, badges, toggles, sidebars, tabs, task cards, section headers, empty/loading states
- Theme-aware template:
templates/theme-aware-app.htmldemonstrating all design patterns with automatic Smoothie/XP switching - Internal hosting docs: updated publishing reference with paste HTML and upload ZIP hosting on ManAurum (no external hosting needed)
- Quick-create API docs:
POST /api/developer/apps/quick-createfor one-step app creation
Changed
- Design guidelines expanded from basic colors/fonts to full component library
- Publishing flow updated to reflect Telegram-style creation (name only, slug auto-generated)
1.0.0 (2026-04-08)
Added
manaurum-appskill — generate apps from prompts with SDK, manifest, theme supportmanaurum-deployskill — hosting setup, publishing (private/unlisted/public)manaurum-setupskill — scaffold new project from scratch- SDK API reference (all postMessage events, SDK methods, 7 permissions)
- Manifest specification (validation rules, field reference, window presets)
- Design guidelines (Smoothie/XP themes, colors, typography)
- Publishing flow (private → unlisted → public, review process)
- Templates: hello-world.html, manifest.json