Modules

Module source layers, manifest, and runtime APIs.

Clapilot modules are filesystem packages with a manifest, frontend entry, optional API handler, optional background workers, and optional migrations.

Source layers and precedence

Modules are discovered from three source layers (discovery logic: src/lib/module-store/local-modules.ts):

  • workspace: <workspace>/modules (default /app/workspace/modules)
  • managed: <state dir>/modules — the state dir resolves from CLAPILOTAICORE_STATE_DIR / CLAPILOT_STATE_DIR / CLAPILOT_HOME, with OPENCLAW_HOME retained as a legacy fallback
  • bundled: CLAPILOT_BUNDLED_MODULES_DIR (legacy OPENCLAW_BUNDLED_MODULES_DIR), default /app/bundled-modules, plus a host-local fallback of <repo>/bundled-modules while running from the repo

Discovery scans each source in parallel and keeps a five-second process-local, single-flight snapshot with direct slug/module-key maps. Install, deactivate, delete, icon, and menu-visibility mutations invalidate the snapshot immediately. The browser independently deduplicates concurrent full-inventory requests from the sidebar and active-module guard.

When the same slug exists in several layers, the effective entry is picked in this order:

  1. a fixed: true bundled module always wins (and cannot be shadowed by a workspace copy)
  2. otherwise the higher manifest version wins
  3. at equal versions the bundled copy is preferred, then the newer manifest mtime, then source priority workspace > managed > bundled
workspace workspace/modules managed state dir/modules bundled /app/bundled-modules resolve per slug fixed > version > layer effective module /modules/<slug>

Bundled install state is instance-wide: explicit overrides live in module_installs, while app_settings.module_install_policy selects minimal for new instances or legacy_all for grandfathered instances. The legacy <workspace>/.clapilot-bundled-modules.json disabled list remains synchronized and is used only when the policy is legacy_all and no DB override exists. Fixed bundled modules ignore all overrides and are always installed. Workspace and managed entries are always installed.

The minimal new-instance set is notizen, excel-canvas, word-canvas, and agents; wiki is additionally present because it is fixed. Migration 214_module_installs.sql assigns legacy_all only to instances that already contain users when the migration first runs, preserving their former effective module set.

Manifest contract

module.json example:

{
  "slug": "my-module",
  "name": "My Module",
  "description": "What this module does",
  "version": "1.0.0",
  "entry": "index.html",
  "renderer": "iframe",
  "workers": [],
  "icon": "boxes",
  "categories": ["productivity", "documents"],
  "hiddenInMenu": false,
  "fixed": false,
  "allowOutsideRead": false,
  "outsideReadRoots": []
}

Validation enforces slug format, semver, safe entry path, and icon normalization.

  • renderer selects how /modules/<slug> renders the module: iframe (default) loads the module's entry HTML in a sandboxed iframe; react maps the slug to a first-party React component in src/components/modules/<slug>-module.tsx (wiring: src/app/(app)/modules/[slug]/page.tsx). Only known first-party slugs have React components; third-party modules use the iframe renderer.
  • workers lists optional background worker entry files inside the module (for example workers/post-publisher.mjs in social-media).
  • hiddenInMenu: true keeps the module active and routable under /modules/<slug>, but removes its automatic left-sidebar menu entry. Admins can toggle this from the local module list without uninstalling the module (POST /api/module-store/set-menu-visibility).
  • fixed: true is reserved for Clapilot-owned bundled modules that must always be active (currently wiki). Fixed bundled modules are read from the bundled source, cannot be installed into the workspace copy, cannot be deactivated, and hide install/remove actions in Module Store.
  • allowOutsideRead / outsideReadRoots let a module's API handler read paths outside its own directory (empty roots list = unrestricted read; used by file-explorer).

Beyond the manifest, some modules are visibility-gated in code (src/lib/module-store/developer-mode-modules.ts): agent-orchestrator and terminal are only visible when Developer mode is enabled (terminal additionally refuses to run without it), and video-studio is admin-only.

Store categories

categories assigns the module to one or more store categories. Allowed keys (see src/lib/module-categories.ts):

  • productivity, communication, documents, legal, finance, marketing, media, automation, developer, insights, utilities

A module can appear in multiple categories at once. Unknown keys are dropped during manifest parsing and logged as a server warning ([module-store] Module "<slug>" declares unknown categories ...); local modules without categories fall back to utilities in the store UI. Category labels are localized (de/en/it) via moduleCategory.* translation keys.

Store icon image

If a module ships an icon.png (or icon.svg/icon.webp/icon.jpg/icon.jpeg) in its root directory, the store uses it as the app icon, served through /api/modules/<slug>/assets/<iconFile>. Without an icon file the store falls back to the matching navigation icon from public/icons/navigation/ or a generated gradient tile with the manifest icon glyph.

Runtime endpoints

  • assets: /api/modules/[slug]/assets/[...assetPath]
  • module API handler: /api/modules/[slug]/api/[...endpointPath]
  • storage API: /api/modules/[slug]/storage

Auth model for the module API handler:

  • web users: clapilot_session cookie
  • native runtime machine calls: Bearer token minted by /api/auth/agent/system-token

HTML assets get window.__CLAPILOT_MODULE_CONTEXT__ injection with apiBase, storageUrl, and assetsBase.

Injected HTML remains no-store. Other module assets use a private five-minute cache, one-hour stale-while-revalidate window, and a weak ETag based on file modification time and size.

All three endpoints resolve only the effective module inventory. An uninstalled bundled slug returns 404 with {"error":"module_not_installed"}. The module page and module-owned settings pages are likewise unavailable, and sidebar/settings entries are filtered out. Native agent-tool gating follows in the companion agent-runtime change.

Context publishing

Modules report page context so chat/live agents know what the user is looking at:

  • React-rendered modules pass a context object to the shell via their onContextChange prop.
  • Iframe modules postMessage their context to the parent window; the module page relays it.

The native runtime maps the active module slug to tool families and action rules via services/clapilot-agent/src/page-capabilities.mjs (for example tax-manager activates the tax_manager_* tools).

Lifecycle: publish and install

  1. discover local modules via Module Store
  2. publish signed archive to hub (admin)
  3. install from hub with integrity checks
  4. for bundled install, run migrations once and persist the instance-wide install override without copying the bundled source

API surface: /api/module-store/* (local, catalog, publish, install, install-bundled, deactivate-bundled, delete, set-menu-visibility).

Store experience

/modules renders an App Store-style storefront for modules, skills, and widgets:

  • category chip filters plus full-text search across name, slug, description, and category labels
  • category sections with two-column app rows (icon, name, description, action pill) and "see all" expansion
  • install / update / uninstall actions: bundled cards show their DB-backed install state, hub modules install from the connected hub, and installed hub modules show an update pill when the hub has a newer version
  • installed bundled cards show a single Installiert pill instead of a separate uninstall button; for admins, one click arms it into a red Deinstallieren pill (auto-reverting after five seconds), and a second click uninstalls the module without opening the detail dialog
  • an app detail dialog with description, categories, version/source/entry metadata, and admin management (menu visibility, publish, deactivate/delete with inline confirmation)

Module icons are supplied by the module and cannot be edited in the store. Store artwork falls back from an unavailable thumbnail to the original image before using the manifest glyph. The retired POST /api/module-store/set-icon endpoint returns 410 Gone for authenticated admins without changing files.

Implementation: src/components/module-store-content.tsx, src/components/skill-store-content.tsx, src/components/mini-apps-content.tsx, shared UI kit in src/components/store/store-app-kit.tsx.

Bundled modules shipped by Clapilot

  • agent-orchestrator (hidden unless Developer mode is enabled)
  • agents (Spezial-Agenten overview)
  • athlete-brand-matching
  • book-appointment (Termine)
  • call-agent
  • canvas
  • cases
  • excel-canvas
  • file-explorer
  • grocery (Einkauf, not installed by default)
  • 3d-studio (3D-Studio, not installed by default)
  • home-assistant (not installed by default)
  • news
  • notizen
  • social-media
  • tax-manager (Finanzen)
  • terminal (hidden unless Developer mode is enabled)
  • video-studio (admin-only)
  • website-canvas
  • wiki (fixed)
  • word-canvas

accounting (Buchhaltung) is a special case: it renders natively in React (src/components/modules/accounting-module.tsx) without a bundled-modules/ directory.

Detailed docs:

Module storage code boundary

Storage write, mkdir and delete actions accept only paths beneath the module data/ directory and reject symlink components. Handler, manifest and entrypoint files require the trusted module authoring/install path. File Explorer treats workspace, managed and bundled module trees as read-only for ordinary callers, including nested roots, aliases and parent deletion; administrators with Developer mode retain privileged authoring access. Existing starter data/notes.txt storage remains supported.

Installed module symlink targets inherit code protection even when their canonical location is in an otherwise writable workspace. Storage policy failures expose stable dataOnly/symlink/codeTarget codes with DE/EN/IT messages. Generated starter storage examples use data/ paths.

Module storage also preserves canonical code targets linked into data/, including deletion of their ancestors, and declared module entry files. Unresolvable module symlink loops remain inaccessible without disabling unrelated ordinary File Explorer writes.

The code boundary also rejects mutation aliases sharing an executable file inode through a hard link. Hard links between ordinary module data files remain usable. Dangling module link chains protect their eventual targets without disabling unrelated workspace writes.

Canonical module-code protection uses a fresh filesystem inventory across workspace, managed, and bundled collections with the same manifest identity validation as module discovery. Code links and shared inodes from any validated module protect their targets from storage mutations in every other module. Ordinary data/ trees are excluded from the code scan; declared entries/workers and links from code into data remain protected. Invalid collection entries and inaccessible subtrees do not disable unrelated File Explorer mutations. Collection directories and inaccessible code subtrees retain their read-only boundary.

A valid manifest establishes module-code protection before its entry exists, preventing activation through dangling links. Manifest paths and shared inodes retain activation protection even while invalid. Storage also treats every canonical module root as code outside that module's own data/, including modules installed inside another module's data directory; both modules' ordinary data remains writable. The shared protection helper is packaged inside File Explorer so relocating a complete module preserves its runtime imports.

Dangling module-directory aliases reserve their eventual manifest path before the directory exists, so a multi-file upload or prepared-directory rename cannot activate code through an otherwise writable canonical target.

Code-link protection includes every intermediate symlink path and reachable cycle member as well as its final target, preventing deletion or renaming of link ancestors. Write-protection inventories do not add module collections to ordinary readable roots. Malformed manifest coercions and non-file manifests are isolated consistently with module discovery and do not disable unrelated mutations; their manifest activation boundaries remain protected.