COLONY — THE ASSISTANTS · v0.4.2.14

The module contract

Source: modules/anthill/docs/MODULE_CONTRACT.md, a path from the root of the product's repository — versioned with the code, rendered here at every release.

v0.3.20.0 — W3-10. How a first-party module enters the colony: one declaration, one registry, one set of fixtures. ADR-007 drew the boundary (a module references the SDK and nothing else of ours; the core never names a module type); this is what a module says about itself across that boundary.

One manifest, nine sections

Anthill.SDK.Modules.ModuleManifest. Every first-party module — tools, reasoning, infrastructure, knowledge, micromound — carries one as public static readonly ModuleManifest Contract and implements IManifestedModule, which adds exactly one member to IAnthillModule: Manifest. The interface stays small on purpose: a manifest is a declaration a module OFFERS, never a hook the host calls into.

Section Fields What it settles
identity and compatibility id, version, platform_api, contracts, dependencies the id is the registry's key and equals Name; platform_api is the SDK contract the module was built against; contracts names the wire and store versions it speaks (micromound.protocol, forager.engine); a dependency is required or optional, with a minimum version
lifecycle runtime, lifecycle in_host, supervised_service (the host starts, verifies, restarts and stops it — FORAGER) or remote_device (mounds dial in; the host never reaches out); the operations it supports, from register · install · configure · start · health · drain · stop · upgrade · remove — a supervised service must declare start, health and stop
user interface ui.door, ui.route_prefixes, ui.settings_pane the area of the rail it owns (Knowledge, Devices), the API routes it maps, the Settings pane that configures it
permissions operation, role, service_scope, approval what each operation needs: a W1-02 project role (viewer · operator · project_admin · owner, or installation for the deployment's own surfaces), the scope a service identity needs, whether it goes through the approval lane
commercial product_id, capabilities, limit_keys, quantity_keys, on_expiry the catalog product, the gated capabilities it grants — never one the catalog marks never_gated — its limit and quantity keys, and what a lapsed subscription leaves (read_only, no_new_allocations); null for a module of the base product
jobs and events jobs.kinds, jobs.event_prefixes the durable job kinds it runs and the event types it publishes
data ownership data.tables, data.stores, data.export_formats, data.retention the tables it owns in the colony's store — every one classified in TenancyTables — the stores it owns outside it, what it exports, who decides retention
operations diagnostics_route, log_prefix, rollback where the technical detail lives (never the main flow), its log prefix, what an upgrade can undo
qualification qualification the cases in qualification/cases.json that exercise it

It fails at build

ModuleManifestValidator is the one rule set — pure, in the SDK, so a module's own tests can run it. ModuleContractTests runs it over every module the build compiles, and then checks what the validator cannot: the manifest agrees with the module's Name and Version; the JSON fixture under packages/contracts/module-manifest/fixtures/valid/<id>.json is structurally equal to the code; every table it owns is one TenancyTables classifies, and the module lists are exactly equal; every route prefix and the diagnostics route name routes Anthill.Api maps; a door is a domain of the console's IA with the same label and route; the commercial vocabulary is the catalog's; every qualification case exists. packages/contracts/module-manifest/manifest.validate.zw.py proves the same fixture set from outside the .NET build — schema, catalog, case list, module tree — and CI runs it as the contracts job.

It refuses at load, and the colony keeps running

ModuleRegistry.Resolve takes the set the composition root hands ModuleHost.LoadAll and returns a plan: which modules load, in an order that puts a dependency before what needs it, and which are refused with a reason a person can act on. A module built against a newer platform API than this host offers is refused. A module whose required dependency the build does not carry is refused; a dependant of a refused module is refused too, and says so. An optional dependency that is missing degrades the module — it loads, the registry notes what it lacks, and what needs it refuses at call time. Nothing in this path throws: the base application stays usable and the failure is explained (A18). The two build defects still throw, as a throwing Register always has: a manifest that does not validate, and two modules claiming one id.

GET /modules carries the registry beside the five-axis door states: platform_api, manifest_schema_version, every loaded manifest in full, modules loaded unmanifested (a third party's zero-core-edit module still loads without one), degraded and refused. Settings → Modules → Registry shows it: one row per manifest, and a refusal in red with its reason.

What this is not

First-party modules only. A public marketplace, untrusted plugin loading, revenue sharing and runtime downloads are later initiatives; nothing here builds a sandbox, and nothing forecloses one — a manifest that arrived as JSON from a package would validate against the same schema, and the registry does not care where a module came from, only what it declares and whether the build can honour it.