Skip to content

System catalog

system.catalog is the unified read view. It owns no state of its own. Every op recomputes from upstream authorities at call time:

system.catalog
  ├── system.registry          → live mount claims
  ├── system.runtimes          → runtime inventory
  ├── system.gateway.http      → webapp route entries (optional)
  └── <each provider mount>    → live $introspect (optional, for filtered searches)

Implementation: projects/matrix-3/packages/system-catalog/src/SystemCatalogActor.ts.

Op surface

typescript
static override accepts: Record<string, unknown> = {
  'catalog.list':     { description: 'List canonical catalog entries derived from system.registry and system.runtimes', prefix: 'string?' },
  'catalog.resolve':  { description: 'Resolve one canonical catalog entry and include provider metadata', mount: 'string' },
  'catalog.roots':    { description: 'List top-level canonical catalog namespace roots', options: 'object?' },
  'catalog.children': { description: 'List immediate child namespace segments under a canonical catalog prefix', prefix: 'string?' },
  'catalog.tree':     { description: 'Return the canonical catalog namespace tree', prefix: 'string?' },
  'catalog.search':   { description: 'Search live runtime catalog surfaces and actor/service mounts', /* ... */ },
};

No catalog.register, catalog.unregister, or catalog.update. The catalog is read-only by design.

How catalog.list is computed

SystemCatalogActor._loadInputs issues three parallel request/reply calls:

  1. system.registry.registry.list → claim records (mount, provider, package).
  2. system.runtimes.runtimes.registered → runtime inventory and metadata.
  3. system.gateway.http.gateway.http.status → webapp route entries (best-effort, absent if the gateway is not mounted).

buildCatalog then joins claim → runtime → inventory metadata into a single ISystemCatalogEntry per logical mount, gathering all providers under the same mount into a providers[] array.

How catalog.search is different

catalog.search accepts the same query shape as system.packages.registry.search (text, prefix, mount, packageName, accepts, emits, skill, kind, inferredKind, declaredKind, shells, status), but it searches live mounts — not installable packages. Use it to answer "what is mounted that accepts chat.send?" not "what packages can I install that accept chat.send?"

For introspection-aware filters (text, accepts, emits, skill, kind), the actor calls $introspect on each candidate mount with { depth: 'full' } and a 1000ms timeout. Failures are silently dropped — providers that don't respond are searched with their bare claim metadata only.

Webapp results

catalog.search results have a source field:

sourceMeaning
actorPlain mounted actor; entry came from system.registry.
webappBrowser-facing app surface; entry came from system.gateway.http route status or from a runtime's metadata.webapp declaration.

Webapps are de-duplicated by (runtimeId, appName) so that the same app appearing in both gateway routes and runtime metadata appears once.

Why this is a separate actor

A read-only join could in principle live in the consumer (Director, dashboards, mx-cli). It lives in its own actor for three reasons:

  1. Caching surface. A future revision can introduce TTL caches on the join without changing every consumer.
  2. Authority boundary. Consumers see one mount (system.catalog) instead of three (system.registry, system.runtimes, system.gateway.http). The underlying join is an implementation detail.
  3. Introspection budget. catalog.search controls how aggressively to call $introspect per query, in one place.

What it does NOT do

  • It does not write to system.registry. To register, call system.registry directly.
  • It does not list installable packages. That is system.packages.registry.search.
  • It does not own webapp routes. The authority is system.gateway.http.
  • It does not own runtime processes. The authority is system.runtimes.

See also

Source: projects/matrix-3/packages/system-catalog/src/SystemCatalogActor.ts.