Skip to content

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:

KindInstalls
appgateway module, console routes, jobs (original behavior)
provider-configLLM provider entry into llm_provider_config
harnessagent/model configuration preset, expressed as importable JSON
mcp-toolMCP server registration
skill / skillsetbehavior packs mounted into the agent sandbox at .agents/skills
cloud-providercloud-adapter implementation plus installer provisioning step

Semantics:

  • Schema: AppManifestSchema.kind is optional (z.enum over the seven supported values; exported as MarketplaceArtifactKind). Invalid kinds are rejected at validation time.
  • Defaulting happens once, at load time. Catalog loading (loadCatalog) and installation (installApp) default an absent kind to 'app'. This preserves backward compatibility with manifests written before ADR 0012. Once loaded or installed, kind is always present.
  • Persistence: marketplace_artifacts.kind stores the resolved kind; InstalledAppRecord, catalog entries, and health results all surface it.
  • Health dispatch: only app artifacts have real health checks today. All other kinds return status: 'not_implemented_for_kind' with healthy: 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 an enabled cloud-provider row (appId = 'cloudflare'). It runs at tenant creation — the gateway registerSelf mutation and the createTenant mutation 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 a core flag (direct Drizzle read per ADR 0010). It backs the GraphQL availableCloudProviders query used by installer/tenant-setup provider selection.
  • Provisioning hook: handleDelegateCloudProject requires exactly one enabled cloud-provider artifact for the tenant before creating a cloud_projects row; 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-case id, name, semver version, optional description, required model config (providerId, modelId, optional temperature / maxTokens / topP), non-empty systemPrompt, optional toolsAllowlist / toolsDenylist, optional skills references to installed skill artifact ids, and a string-valued metadata map. Exported types: HarnessSpec, HarnessModelConfig. Parsers: parseHarnessSpec (throws) and safeParseHarnessSpec (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 against HarnessSpecSchema before persisting into the existing marketplace_artifacts table (kind = 'harness'; validated spec stored in manifest_json, config_json stays {}). No new tables or migrations.
  • The GraphQL surface for create/import/list/get/delete lives in services/gateway/src/schema/harness.ts as 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 status installed or enabled mount. skill artifacts store files in configJson.files ({ path, content }[], paths relative to .agents/skills/<skill-id>/); when absent, configJson.skillMd becomes a single SKILL.md. skillset artifacts declare member skill ids via configJson.members and 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-uninstalled skill / skillset rows for a tenant from marketplace_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 a skillsManifest (root, files, skillIds, mountedAt) in DO storage, and exposes it via getSkillsManifest() and the DO’s GET /skills-manifest route 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 -f through 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?) reads registry.json from the specified repository path, parses the app entries, loads each app’s manifest.json, validates against AppManifestSchema, and returns AppManifest[]. The optional ref parameter 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, returns InstalledAppRecord with status='installed'
    • uninstallApp(appId, purgeData?) — Removes registration, default purgeData=false
    • enableApp(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) returns AppHealthResult with:

    • healthy: boolean — overall health
    • checks — per-component boolean flags: dbSchemaLoaded, gatewayModuleLoaded, domainSchemasRegistered, consoleRoutesInjected, jobsDeployed, imagesBuilt
    • errors: string[] — any error messages

    Currently a stub returning all checks true; real implementation in Wave 5.

Key Components

FileRole
src/app-manifest.tsAppManifestSchema (Zod) + AppManifest type — canonical app manifest definition
src/app-registry.tsloadCatalog(), refreshCatalog() — catalog I/O and diff
src/loader.tsinstallApp(), uninstallApp(), enableApp(), disableApp(), seedCoreCloudProviderArtifact(), listAvailableCloudProviders() + core cloud-provider guard helpers
src/health.tsassessAppHealth() + AppHealthResult type
src/index.tsRe-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/db for persistence and @mnemose/domain for command/event emission
  • Gateway — Consumes AppManifest for boot-time schema/module loading
  • CLI — Uses loadCatalog() for mnemose app install and related commands
  • Fleet UI — Consumes AppHealthResult for health dashboards

Dependencies

  • zod — Runtime validation for AppManifest
  • @mnemose/db (peer) — Future: InstalledAppRecord persistence
  • @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
  • AppStatus is 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.checks without breaking changes
  • Create-if-missing DB schema application — an artifact’s dbSchemaModule SQL file is applied to the tenant D1 database as-is on install (applyAppDbSchema in db-schema.ts, invoked by installFromSource). Statements execute atomically in a single db.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.