marketplace — Architecture
The @mnemose/marketplace package is the core framework for Mnemose app lifecycle management. It provides the single source of truth for AppManifest validation, catalog loading, app registration, and health assessment.
Artifact Kinds (ADR 0012)
Every marketplace artifact carries a kind discriminator identifying what the
artifact installs:
| Kind | Installs |
|---|---|
app | gateway module, console routes, jobs (original behavior) |
provider-config | LLM provider entry into llm_provider_config |
harness | agent/model configuration preset, expressed as importable JSON |
mcp-tool | MCP server registration |
skill / skillset | behavior packs mounted into the agent sandbox at .agents/skills |
cloud-provider | cloud-adapter implementation plus installer provisioning step |
Semantics:
- Schema:
AppManifestSchema.kindis optional (z.enumover the seven supported values; exported asMarketplaceArtifactKind). Invalid kinds are rejected at validation time. - Defaulting happens once, at load time. Catalog loading
(
loadCatalog) and installation (installApp) default an absentkindto'app'. This preserves backward compatibility with manifests written before ADR 0012. Once loaded or installed,kindis always present. - Persistence:
marketplace_artifacts.kindstores the resolved kind;InstalledAppRecord, catalog entries, and health results all surface it. - Health dispatch: only
appartifacts have real health checks today. All other kinds returnstatus: 'not_implemented_for_kind'withhealthy: false— health assessment never fakes a healthy result for a kind it cannot actually check.
Cloud Provider Artifacts (M-4)
The tenant’s active cloud is modeled as a marketplace artifact with
kind = 'cloud-provider' (ADR 0012, stripe M-4). Cloudflare is currently the
only registered cloud adapter (packages/cloud-adapters), so every tenant is
seeded with a core cloudflare artifact:
- Seeding:
seedCoreCloudProviderArtifact(db, tenantId)idempotently inserts anenabledcloud-providerrow (appId = 'cloudflare'). It runs at tenant creation — the gatewayregisterSelfmutation and thecreateTenantmutation both call it. No new tables or migrations. - Core guard: the core artifact cannot be uninstalled or disabled.
There is no schema-level core flag; instead the guard is enforced in code
via
assertNotCoreCloudProvider(kind, appId)(exported from the loader and re-checked by the app-lifecycle job handler) so uninstall/disable of the protected pair is rejected with an error. Ordinary apps are unaffected. - Availability query:
listAvailableCloudProviders(db, tenantId)returns registered non-uninstalled cloud-provider artifacts for a tenant with their status and acoreflag (direct Drizzle read per ADR 0010). It backs the GraphQLavailableCloudProvidersquery used by installer/tenant-setup provider selection. - Provisioning hook:
handleDelegateCloudProjectrequires exactly one enabled cloud-provider artifact for the tenant before creating acloud_projectsrow; delegation fails otherwise.
Harness Specs (M-3-A)
Harness artifacts (kind = 'harness') are agent/model configuration presets
expressed as importable JSON documents, defined by src/harness-schema.ts:
HARNESS_SPEC_VERSION('1') — spec document format version.HarnessSpecSchema(Zod, strict — unknown keys rejected): kebab-caseid,name, semverversion, optionaldescription, requiredmodelconfig (providerId,modelId, optionaltemperature/maxTokens/topP), non-emptysystemPrompt, optionaltoolsAllowlist/toolsDenylist, optionalskillsreferences to installed skill artifact ids, and a string-valuedmetadatamap. Exported types:HarnessSpec,HarnessModelConfig. Parsers:parseHarnessSpec(throws) andsafeParseHarnessSpec(safe), both accepting JSON strings or objects.- One validation layer, three ingress paths: marketplace catalog install,
raw JSON import, and console create all funnel through
installHarness(db, spec, tenantId), which validates againstHarnessSpecSchemabefore persisting into the existingmarketplace_artifactstable (kind = 'harness'; validated spec stored inmanifest_json,config_jsonstays{}). No new tables or migrations. - The GraphQL surface for create/import/list/get/delete lives in
services/gateway/src/schema/harness.tsas direct-write resolvers per ADR 0010; the console harness UI is a later stripe and will call this surface.
Skill Mounting (M-3-B)
Installed skill artifacts materialize into the agent sandbox filesystem at
.agents/skills/ (relative to the workspace root) so they appear in the
agent’s wake-up context, matching the existing .agents/skills convention.
- Planning (
src/skill-mount.ts, pure):planSkillMountFiles(records)translates installed records into a deterministic file tree. Only records with statusinstalledorenabledmount.skillartifacts store files inconfigJson.files({ path, content }[], paths relative to.agents/skills/<skill-id>/); when absent,configJson.skillMdbecomes a singleSKILL.md.skillsetartifacts declare member skill ids viaconfigJson.membersand expand to those members’ files; members without an installed backing skill are skipped. Planning is idempotent — the same inputs always yield the same plan. - Query surface:
getInstalledSkills(db, tenantId)loads non-uninstalledskill/skillsetrows for a tenant frommarketplace_artifacts(direct-read per ADR 0010). - Mount step:
SandboxDO.mountInstalledSkills(db)runs during sandbox provisioning (best-effort — readiness is never blocked by mounting). It uploads each planned file to<workspace-root>/.agents/skills/...via the execution adapter’s upload path, persists askillsManifest(root,files,skillIds,mountedAt) in DO storage, and exposes it viagetSkillsManifest()and the DO’sGET /skills-manifestroute for the future wake-up context builder and Workbench wiring. - Idempotency & pruning: re-mount overwrites planned files cleanly;
previously-mounted paths absent from the current plan are removed with a
best-effort
rm -fthrough the adapter (diff-and-prune), so uninstalled or removed skills do not linger. Failures are recorded per-phase on the mount result rather than thrown.
Responsibilities
-
AppManifest Schema — Define and export the canonical Zod schema (
AppManifestSchema) and inferred TypeScript type (AppManifest) for all app metadata, including identity, source, store metadata, modules, commands, events, MCP servers, images, jobs, console integration, secrets, ports, and dependencies. -
Catalog Loading —
loadCatalog(repoPath, ref?)readsregistry.jsonfrom the specified repository path, parses the app entries, loads each app’smanifest.json, validates againstAppManifestSchema, and returnsAppManifest[]. The optionalrefparameter allows loading from a specific git ref (branch/tag/commit). -
Catalog Refresh —
refreshCatalog()performs a diff between the current in-memory catalog and the live repository, returning{ added, updated, removed }arrays. This is a stub in Wave 1; full implementation in Wave 5. -
App Registration (Loader) — Pure in-memory registration functions:
installApp(manifest)— Idempotent registration, returnsInstalledAppRecordwithstatus='installed'uninstallApp(appId, purgeData?)— Removes registration, defaultpurgeData=falseenableApp(appId)— Sets status to'enabled'disableApp(appId)— Sets status to'disabled'
These functions do NOT emit domain events (CQRS handler responsibility) and do NOT modify the running gateway schema (applied at boot time). They are pure registration operations.
-
Health Assessment —
assessAppHealth(appId)returnsAppHealthResultwith:healthy: boolean— overall healthchecks— per-component boolean flags:dbSchemaLoaded,gatewayModuleLoaded,domainSchemasRegistered,consoleRoutesInjected,jobsDeployed,imagesBuilterrors: string[]— any error messages
Currently a stub returning all checks
true; real implementation in Wave 5.
Key Components
| File | Role |
|---|---|
src/app-manifest.ts | AppManifestSchema (Zod) + AppManifest type — canonical app manifest definition |
src/app-registry.ts | loadCatalog(), refreshCatalog() — catalog I/O and diff |
src/loader.ts | installApp(), uninstallApp(), enableApp(), disableApp(), seedCoreCloudProviderArtifact(), listAvailableCloudProviders() + core cloud-provider guard helpers |
src/health.ts | assessAppHealth() + AppHealthResult type |
src/index.ts | Re-exports all public API |
Data Flow
App Repository (registry.json + manifests) ↓ loadCatalog(repoPath, ref?)AppManifest[] (validated) ↓ installApp(manifest)InstalledAppRecord (status='installed') ↓ enableApp(appId)InstalledAppRecord (status='enabled') ↓ assessAppHealth(appId)AppHealthResult { healthy: true, checks: {...}, errors: [] }Integration Points
- Orchestrator — Wires loader functions to
@mnemose/dbfor persistence and@mnemose/domainfor command/event emission - Gateway — Consumes
AppManifestfor boot-time schema/module loading - CLI — Uses
loadCatalog()formnemose app installand related commands - Fleet UI — Consumes
AppHealthResultfor health dashboards
Dependencies
zod— Runtime validation for AppManifest@mnemose/db(peer) — Future:InstalledAppRecordpersistence@mnemose/domain(peer) — Future:InstallAppCommand,EnableAppCommand, etc.
Conventions
- All functions are pure (no side effects beyond in-memory state)
- Validation errors throw Zod errors; callers handle
AppStatusis a discriminated union for type-safe state transitions- Catalog operations are stateless;
refreshCatalog()uses internal cache for diff - Health checks are extensible; new checks added to
AppHealthResult.checkswithout breaking changes - Create-if-missing DB schema application — an artifact’s
dbSchemaModuleSQL file is applied to the tenant D1 database as-is on install (applyAppDbSchemaindb-schema.ts, invoked byinstallFromSource). Statements execute atomically in a singledb.batch(). Artifact schemas MUST be authored idempotently (CREATE TABLE IF NOT EXISTS,CREATE INDEX IF NOT EXISTS); the platform never diffs, migrates, or drops app-owned tables, so reinstalls are no-ops and uninstall never touches app data.