Docs
/

@leumas/app-kit

The ONE contract every Leumas app complies with. Declare an app's actions once and get five surfaces free: adapter functions, Leviathan/chatbot functioncalls, an MCP server, a REST router, and a...

@leumas/app-kit

One declaration → a store listing, a public catalog row, an MCP server, Leviathan functioncalls, a REST router and a generated SDK.

Two files carry the whole package:

  • src/define.jsdefineLeumasApp(spec), what an app IS.
  • src/host.jsappFacade() and mountApps(), what an app may DO. **This is the security core

of the app system**; read its header before changing anything in it.

Authoring guide (including the sandboxed tier, which this package does not cover): shared/services/knowledge/build-knowledge/build-a-plugin.md.


defineLeumasApp(spec)

FieldTypeNotes
idstringrequired, slugged. Becomes the namespace for everything below.
name, description, icon, version, categorystringstore-facing identity
permissionsstring[]validated at definition time — an unknown value throws on import
collections`Array<string \{name, scope, admin}>`→ table app_<id>_<name>; scope defaults 'tenant'
actions{ [name]: { description, inputs, price, http, run } }needs actions or routes
routes[{ path, surface, index }]Studio surfaces
nav, surfacesthe mount manifest
basePath / apiBasestringdefault /apps/<id> and /api/apps/<id>
mountType`'module' \'iframe'`third-party is always iframe, whatever this says
pricing{ model, price, unit, planKey, meterTargets }declarative — see below
catalogobjectstorefront copy; summary, tags, screenshots[], …
domainsstring[]Studio domains this unlocks — honoured for first-party only
publisher, homepage, repo, featureKey, entitlements

Returns { metadata, adapters, manifest, toMcpServer(), functioncalls(), createRouter(), createSdk(), listing(), catalogEntry(), setContext(), getContext() }.

pricing is a request, not an enforcement

What is enforced is a passnode_rules row. For a third-party app it is written by the review desk at publish, with owner taken from the stored listing — so a submission can neither price itself nor nominate someone else as payee. For a first-party app it is written at boot by hydrateAppCatalogs({ writeRule }), because a first-party app never goes through review.

Before that boot hook existed, pricing on a first-party app was a number on a store card and the action was free.

collections[].scope is load-bearing

'tenant' (the default) makes ctx.db filter by tenant: list injects it, get/update/remove verify ownership first, and a foreign row reads as null, not 403 — a 403 would let an app enumerate which ids exist in another customer's data.

'global' opts out. It has to be declared, so sharing a table across every customer on an instance is a decision rather than an oversight.


The permission vocabulary

Validated against PERMISSIONS in define.js; an unknown one throws at definition time, because a silently-ungranted permission fails later as a confusing undefined.

PermissionGrantsPresent as
db:ownthe app's own namespaced collectionsctx.db
db:read:<collection>one named collection, read-onlyctx.readOnly
storagea prefixed file area (apps/<id>)ctx.storage
jobs, triggersbackground work, trigger firingctx.jobs, ctx.triggers
llmmodel calls, metered to plugin:<id>.llmctx.llm
adapters:<system>.<fn>exactly that functionctx.adapters.run
mcp:<server>an MCP server
network:<host>outbound fetch to that hostctx.fetch
fs, devices, computeappliance-only, also gated by the deployment's ROLE_CAPSctx.fs etc.

appFacade(app, deps, extra) — absent, not refused

The one sentence that matters: an app never receives the raw dependency context. It receives a facade built from the permissions it declared, and anything it did not declare is ABSENT from that object rather than merely refused.

const ctx = appFacade(app, deps, { user, tenantId });
ctx.db     // present iff `db:own` AND deps.adapter
ctx.fs     // undefined unless BOTH the app declared `fs` and the deployment offers it

Absent rather than throwing is deliberate three ways: a ctx.db that exists and throws teaches an app to try; an undefined one cannot be reached for at all; and a permission audit becomes a property check rather than a code review.

Declaring is necessary, never sufficient. A capability appears only when the app asked for it AND the host passed the dependency — so an approved listing cannot import filesystem access into a deployment that has none.

The install record, not the listing

For a sandboxed plugin the facade is built with permissions: install.grantedPermissions. That single choice is what makes the update prompt a boundary: a v1.1 that adds fs has no ctx.fs on an instance that agreed to v1.0, whatever the listing now says.


mountApps(apps, opts)

const host = mountApps(FIRST_PARTY_APPS, {
  Router: express.Router,
  deps: { adapter, adapterRegistry, triggers },
  gate: requireAuth,
  meter: (target) => passnode.guard(() => target),
  requestScope: (req) => ({ user: req.user, tenantId: req.user?.tenantId }),
});

requestScope builds a per-request facade, and without it the tenant scoping above is dead code: the module-level context is bound once at boot and knows no caller, so tenantId is null and nothing is filtered.

isInstalled is the wrong tool for install state

mountApps accepts it, and it is only correct for build-or-deployment facts ("is this app in this edition?"). For INSTALL state it fails three ways, and the third is fatal:

  1. it runs at boot, before backfillInstalls — so on the upgrade that introduced install gating,

every router vanishes for an operator who uninstalled nothing;

  1. its signature has no tenant, while installs are per-tenant;
  2. Express has no unmount, so the decision is stale the moment anyone installs anything.

Install gating happens per request, in front of the mounted router — @leumas/plugin-host's createInstallGate.


The store, review, installshared/services/marketplace/
Sandboxed tier: bundles, proxy, gateshared/services/plugin-host/
The iframe protocolshared/packages/plugin-bridge/
Guardspnpm smoke:apps · check:apps · smoke:plugin-proxy · check:sandbox

What it exports

3 exports: 0 components, 0 hooks, 3 helpers. Import from @leumas/app-kit.

appFacadedefineLeumasAppmountApps
Source shared/packages/app-kit/README.md (no-git)markdownjson
Generated from the Leumas repository. Every page cites the file it came from.leumas.techllms.txt