Extension IoC Safeguards — keeping the extension system truly extensible
This page is the durable control that keeps the extension system extensible: an executable definition of “extensible”, the CI gates that make a regression un-mergeable, and the review contract every change is held to. A genuinely extensible system is the prerequisite for live runtime install and uninstall — you cannot hot-uninstall a capability that core has statically wired.
The principle
Section titled “The principle”Core defines the kinds of extension (agent / connector / artifact / skill /
workflow) and discovers instances dynamically from the installed_extension
manifest. Core must never name, import, or hardcode a specific extension
instance. Capabilities come from the manifest plus the per-kind native stores via
the runtime-discovery dispatcher — never from if (packageName === "@cinatra-ai/foo"), a hardcoded @scope/pkg string, or an
extensions/<scope>/<name>/… path.
The single active-manifest dispatcher (split-brain guard)
Section titled “The single active-manifest dispatcher (split-brain guard)”There is exactly one discovery dispatcher,
discoverActiveExtensionCapabilities, and it is the only authority on “what is
live right now”. It resolves discovery as:
discovery = (installed_extension rows with status active|locked) ∩ (per-kind visible native rows for the actor/scope)This intersection is the split-brain guard. The canonical
installed_extension manifest is the lifecycle authority; the per-kind native
store holds the kind’s content (an agent template, a connector config, an
artifact renderer, a skill, a workflow template). Neither side alone may decide
visibility:
- An archived or uninstalled extension is suppressed from discovery even if a stale native row survives — the manifest side of the intersection is empty.
- A
lockedextension stays discoverable (it is still live) —lockedis in theactive|lockedset. - A native row with no manifest row is not discovered — discovery never surfaces ungoverned content.
Because every surface goes through this one dispatcher, archive / uninstall / restore take effect immediately and uniformly, with no surface holding its own divergent view of “what is installed”.
The executable definition of “extensible” (CI-enforced)
Section titled “The executable definition of “extensible” (CI-enforced)”“Extensible” is the conjunction of these four checks, all required in CI. If they pass, the system is extensible by definition; if any fails, it is not.
| Control | What it proves | Where |
|---|---|---|
| Runtime contract — golden conformance test | discovery = lifecycle-live manifests (active|locked) ∩ per-kind visible native rows; archive / uninstall suppress stale native rows; locked stays discoverable | packages/extensions/src/__tests__/extension-discovery-conformance.test.ts (the Canonical extension invariants required job) |
| Structural contract 1 — import-ban | no NEW core import of a specific extension package | scripts/audit/core-extension-import-ban.mjs (shrink-only baseline) |
| Structural contract 2 — instance-coupling | no NEW core string / path reference to a specific extension instance | scripts/audit/core-extension-instance-coupling-ban.mjs (shrink-only baseline) |
| Structural contract 3 — discovery-bypass | no NEW surface reads a native store directly instead of the dispatcher | scripts/audit/discovery-dispatcher-bypass-ban.mjs |
The runtime conformance test is the canonical behavioral truth: it asserts
the discovery = active-manifests ∩ visible-native-rows intersection directly,
including that an archived or uninstalled extension’s stale native rows are
suppressed and that locked rows remain discoverable. The three structural gates
are the canonical structural truth.
The static-list / instance-naming bans
Section titled “The static-list / instance-naming bans”The two coupling vectors that let core drift back toward naming a specific extension are banned outright:
- Import-ban — no NEW core
importresolves to a specific extension package. Core depends on the SDK contract and the dispatcher, not on any one extension. - Instance-coupling ban — no NEW core string literal, JSX text, schema
description, prompt, path, or package-metadata branch names a specific
extension instance. This closes the gap that a pure import-ban misses (a
hardcoded package name in a string, or a hand-built
extensions/<scope>/<name>path).
Both are no-new-rot ratchets: the baseline can only shrink. A monotonic
guard (*_BASE env compared against the base ref, with a fail-closed
rev-parse) blocks regenerating the baseline to pass, and decoupling work drives
each baseline toward 0.
The @/-import ban (core stays decoupled from extensions)
Section titled “The @/-import ban (core stays decoupled from extensions)”Extensions are forbidden from reaching into core via the @/ app alias. An
extension consumes the host only through the SDK port surface it is granted
(ctx.settings, ctx.secrets, ctx.objects, ctx.mcp, and the rest of the 14
host ports on SDK ABI 2), never through a direct @/lib/... or
@/components/... import. This keeps the dependency arrow pointing one way:
extensions depend on the SDK; core depends on neither a specific extension nor an
extension’s internals. It is what makes an extension cleanly extractable into its
own package and cleanly removable at runtime.
The standing review contract
Section titled “The standing review contract”Every change touching extensions or discovery is held to this checklist. A “no” on any line blocks the change.
- No instance naming. No core import, string literal, JSX text, schema
description, prompt, path, or package-metadata branch names a specific
extension instance — except existing ratcheted baseline debt or
src/lib/generated/**(the legit data-driven install list). - Discovery via the dispatcher. Discovery surfaces call
discoverActiveExtensionCapabilities; direct native readers are allowed only inside the sanctioned per-kind handlerlistActivefacet. - Scope preserved. Discovery passes
actor/scopeand preserves visibility filtering; no unscoped native reads. - Lifecycle authority. Archive / uninstall / restore use the canonical
installed_extensionmanifest as the lifecycle authority; stale native rows must not reappear in discovery (no split-brain). - Kind-generic, not instance-specific. Per-kind code is generic over the
kind; instance behavior comes from the extension’s manifest / capabilities /
metadata, never
if packageName === …. - Generic routing. UI / render / tool routing prefers generic capability and renderer registries; package-prefixed renderer keys are baseline debt unless specifically migrated.
- New kinds. A genuinely new extension kind ships a conformant
listActivefacet plus a per-kind intersection test. (New instances need no code — that is the point.) - Ratchets only shrink. No no-new-rot baseline grows. If a change removes debt, it regenerates the relevant baseline in the same change.
- CI wiring is part of the contract. The gates and their tests stay required checks.
How to decouple (the cutover pattern)
Section titled “How to decouple (the cutover pattern)”When a surface statically couples to an extension instance: replace the hardcoded reference with a manifest / registry lookup (the dispatcher for discovery; the catalog or native store via the kind handler for content), then regenerate the relevant baseline so the gate locks the reduction. Each removed reference is permanent — the ratchet cannot let it back.
See also: developer guide index · Extensions engineering reference · Extension lifecycle and distribution · Extension access contract