Extension-coupling audit gates — classification, exemption policy, end-state
This document is the reference the extension-coupling gates point at. It
defines the shared reference taxonomy, the strict exemption policy, and the
zero-floor end-state: no hand-written host code imports or names a concrete
extension, no extension imports host @/ modules / other extensions /
non-SDK first-party packages, and the gates are pinned so none ever can
again. The end-state was reached in BOTH directions of the
inversion-of-control (IoC) rule — core→extension and extension→host.
The gates
Section titled “The gates”| Gate | Direction | Unit | Baseline |
|---|---|---|---|
core-extension-instance-coupling-ban.mjs |
core (src/ + packages/) naming a specific extension (string/JSX/prompt/metadata literal, path literal, or import) |
file :: kind :: value -> count occurrences |
core-extension-instance-coupling-ban.baseline.json — PINNED EMPTY |
core-extension-import-ban.mjs |
core (src/) importing an extension package |
file -> extension edges |
core-extension-import-ban.baseline.json — PINNED EMPTY |
extension-import-ban.mjs |
extensions importing host @/ modules, other extensions, or non-SDK first-party packages |
extension -> module edges in 3 dimensions |
extension-import-ban.baseline.json — PINNED EMPTY |
required-extensions-cover-host-imports.mjs |
the prod bootable DECLARATION vs the live code surface | packages | live-derived (no baseline) + the declaration equality guard |
discovery-dispatcher-bypass-ban.mjs guards the runtime-discovery dispatcher
(its documented SANCTIONED_READERS allowlist is “sanctioned, never counted” —
distinct from the baseline, which is pinned EMPTY since the zero-tolerance flip).
host-peer-value-import-ban.mjs holds every serverEntry graph at 0 host-peer
value imports (SDK peers stay type-only).
Enforcement model — the zero-floor end-state
Section titled “Enforcement model — the zero-floor end-state”FOUR baselines are PINNED EMPTY — zero is the floor AND the ceiling, in BOTH directions of the IoC rule:
core-extension-instance-coupling-ban(the instance-coupling flip): any non-comment occurrence of an extension package name orextensions/<scope>/<name>/path literal in core source fails CI immediately; a non-empty committed baseline is itself a failure;--write-baselinerefuses non-empty output. The frozenSCANNER_EPOCH(=2) and theCORE_EXT_INSTANCE_BAN_BASEmonotonic guard survive purely as tamper checks (fail-closed on unresolvable refs / any epoch mismatch).core-extension-import-ban(the import honest-zero flip, landed WITH the shared-lexer adoption + the last transport edges’ removal): any core->extension import edge fails immediately; same non-empty-baseline and--write-baselinerefusals;CORE_EXT_BAN_BASEkept as a tamper check.discovery-dispatcher-bypass-ban(the zero-tolerance flip): any non-sanctioned direct native-reader reference fails immediately.extension-import-ban(the extension→host direction, completing the IoC rule’s zero floor in both directions): any currenthostInternal,crossExtension, orsdkOnlyedge fails immediately (the committed baseline is no longer consulted for violation detection); a committed baseline with any non-empty dimension is itself a failure;--write-baselinerefuses non-empty output;--strict-sdk-onlyis retained as an accepted no-op (thesdkOnlydimension is unconditionally zero-tolerance — neither passing nor omitting the flag can weaken enforcement); the owner-ruledSTRICT_SDK_ONLY_ALLOWLIST(EMPTY, self-policing via the stale-carve-out hard failure) is the only carve-out mechanism, scoped to thesdkOnlydimension exclusively;IMPORT_BAN_BASEsurvives purely as a fail-closed tamper check.
On top of the pinned-empty gates:
- the cover gate enforces the declaration equality
requiredExtensions == systemExtensions == lockON TOP of its live bootable-coverage derivation: the prod bootable declaration may not grow beyond the system set without an owner ruling that also declares the package a systemExtension. The equality pins the DECLARATIONS only — regrowth of hard-coded extension names in code is caught by the two pinned-empty coupling gates, and an undeclared hard import is caught by the live coverage derivation, not by the equality.
Changing any of this requires editing the gate code and its tests in a reviewed PR — there is no data path (baseline, epoch, seed, regenerate) that can raise a floor.
Reference classification (shared taxonomy)
Section titled “Reference classification (shared taxonomy)”Defined in scripts/audit/lib/extension-reference-classification.mjs and used
by the coupling gates:
- runtime-coupling — core selects/loads/branches on a specific extension at runtime (named imports, loader maps, provider registration, prompt/dispatch literals). The default class; ZERO occurrences remain — any reappearance fails the pinned-empty gates.
- mechanical — re-export facades, hand-written inventories/catalogs, and dev-name lists. Counted exactly like runtime-coupling — never exempt; at ZERO since the mechanical-cleanup phase.
- permanent-exempt — never counted. Strict, owner-ruled set; see below.
Strict exemption policy
Section titled “Strict exemption policy”Permanently exempt are ONLY:
- The generator-emitted file list — the exact files
scripts/extensions/generate-extension-manifest.mjsemits (the sharedGENERATED_MANIFEST_FILESlist:extensions.server.ts,connector-setup-pages.ts,extensions.client.tsx,widget-stream-public-paths.ts,agent-bindings.tsundersrc/lib/generated/, plus the ONE package-local emissionpackages/objects/src/generated/artifact-floor.ts— the semantic-floor binding lives insidepackages/objectsbecause that package is consumed from graphs where the host@/alias does not resolve; same generator, same byte pin, same explicit-list discipline — the exempt class is the EMITTED LIST, not a directory). Names there are generator output — the legitimate data-driven install list, not hand-coupling. The owner ruling made the generator-emitted set the ONE permanent-exempt class (the sibling generated maps are part of it, not a separate concession), unifying the instance-coupling and import-ban exempt sets. Two integrity guards keep the exemption honest:- the exemption is an EXPLICIT file list, never a directory prefix — a
hand-added extra file under
src/lib/generated/(or anygenerated/dir) is counted (default class runtime-coupling → hard fail); - the listed files are pinned to the generator’s byte-exact output by the
FAIL-CLOSED
generate-extension-manifest.mjs --checkCI step (drift, missing file, or catalog-parity break fails CI).
- the exemption is an EXPLICIT file list, never a directory prefix — a
hand-added extra file under
- The documented data-contract-ID allowlist
(
DATA_CONTRACT_ID_ALLOWLIST) — stable string identifiers that embed an extension name as a frozen serialization/compatibility contract, NOT as runtime selection. Every entry must carry a written justification (the gate hard-fails on an unjustified entry), entries are added only with an owner ruling, stale entries hard-fail until removed, and allowlisted occurrences are reported separately from counted ones. IDs may contain ONLY the boundary alphabet[A-Za-z0-9_.:/@-](DATA_CONTRACT_ID_ALPHABET_RE) — enforced as a structural defect — so the exact-ID masking can never prefix-mask a longer ID past a non-alphabet character. EMPTY at the zero-floor end-state — it stays empty unless an owner ruling mints an entry. - Test files (
*.test.*,*.spec.*,__tests__/,__mocks__/,tests/) and theextensions/tree itself (an extension naming itself is fine).
No facades, no inventories, no dev-name lists are exempt — they are counted
(mechanical) and hard-fail if they ever reappear.
Known, documented residual lexer limitation: JSX TEXT is not modeled by
lib/strip-comments.mjs (that needs a JSX-aware parser), so a named-extension
reference appearing in JSX text AFTER a bare non-URL // on the same line
would be under-counted. No such case exists in the tree. There is no
epoch-recompute path: if a future JSX-aware lexer reveals references, they
must be fixed in the same PR that lands the lexer (the floor cannot rise).
That policy was exercised by the import-ban scanner itself: its legacy regex
stripper (blind after a line comment containing a literal /*) was replaced
by the shared lexer in the SAME PR that removed the four transport-DI edges
it had been hiding — every CORE-side coupling scanner (instance-coupling,
import-ban, the cover gate’s hard-import scan) now runs the shared lexer.
(extension-import-ban — the reverse direction — still strips comments via
its own inventory tooling; its floors are PINNED EMPTY since the
extension→host flip, so a stripper correction there that reveals edges must
land WITH those edges’ removal in the same PR — the identical
fix-with-the-reveal policy, with no floor that can rise.)
Pinned floors — the zero-floor end-state
Section titled “Pinned floors — the zero-floor end-state”| Gate | Pinned floor | Direction |
|---|---|---|
core-extension-instance-coupling-ban |
0 occurrences / 0 keys / 0 files | PINNED EMPTY |
core-extension-import-ban |
0 edges / 0 files | PINNED EMPTY (honest under the shared lexer) |
discovery-dispatcher-bypass-ban |
0 files (5 documented sanctioned readers, justified in-gate) | PINNED EMPTY |
extension-import-ban |
0 @/ + 0 cross-extension + 0 sdkOnly (allowlist EMPTY) |
PINNED EMPTY |
host-peer-value-import-ban |
0 over all serverEntry graphs | hold at 0 |
| cover gate declarations | requiredExtensions == systemExtensions == lock == 8 (0 hard-imported, 8 generated-required, 0 root-dep; every other extension guardedOptional/acquirable-on-demand) | equality, live-enforced |
Root + package-level concrete connector workspace:* deps |
0 | hold at 0 |
The journey (for the record): the corrected instance-coupling baseline started
at 349 occurrences / 96 import edges; over multiple decoupling phases —
plus a lazy/guarded cutover — it was driven down through an intermediate
166/41 floor and finally emptied to zero pinned floors across all six gates.
The core→extension direction was emptied in stages: a nango serverEntry cutover
(import-ban 10→0), provider-adapter extraction, a transport dependency-injection
inversion (import-ban pinned empty + shared lexer adopted), connector-edge and
catalog-metadata cleanup (the requiredExtensions floor of 8 reached),
agent-identity decoupling (the single largest drop), and an
artifact/blog/seed tail that emptied the baseline; a final flip pinned the
zero and the declaration equality. requiredExtensions shrank 16 → 8 ==
systemExtensions along the same train.
The REVERSE direction (extension→host) drove extension-import-ban’s
hostInternal dimension 16 → 12 → 8 → 4 → 0 → PINNED EMPTY across five
stages: CRM ctx-port adoption plus two test re-groundings; the Drupal family
(a Drupal MCP service extension + a new Drupal widget-auth service); the
WordPress family (a WordPress MCP connection-admin extension + new
WordPress content and widget-auth services); the transport tail (new GitHub /
LinkedIn / YouTube connection services + the external-MCP-registry read
surface); and the pinned-empty flip (crossExtension and sdkOnly were
already empty). All FOUR coupling baselines are pinned empty from that flip
onward.
End-state record — how core reaches extensions now
Section titled “End-state record — how core reaches extensions now”The residual floor register is retired (nothing residual is left). The SANCTIONED inversion-of-control paths, each with its own guard:
- The generated manifest tree (
GENERATED_MANIFEST_FILES): the generator — driven by extensionpackage.jsondeclarations — is the ONE place concrete extension names appear outsideextensions/and tests. Byte-pinned by the fail-closedgenerate-extension-manifest.mjs --checkCI step; loader entries carry generator-ownedresolutionmetadata (requiredforcinatra.systemExtensionsmembers, elseguardedOptionalrouted through the standardized degraded-result guard and proven degradable by the generated test). The presence-degraded build job asserts the regime-aware emission (system-only universe ⇒ zero guarded loaders). - The capability registry: connectors/agents self-register surfaces from
their
serverEntryregister(ctx)(nango-system, llm-provider-surface, crm-list-reader, email-sender-identities, appointment-schedules, transport deps, …); the host publishes per-concern@cinatra-ai/host:*services (register-host-connector-services.ts— names NO extension package) and resolves extension surfaces at call time with established fail-loud or degrade-to-empty semantics per consumer. The legacy delegating-adapter capability id is FULLY retired (contract removal followed by compat-shim removal — the id resolves to nothing; a pre-removal runtime package-store digest gets a capability-resolution miss at call time and must be refreshed from the marketplace). - Manifest metadata bindings:
cinatra.fieldRenderers,cinatra.roles,cinatra.facadePrimitives,cinatra.devCliModules— validated fail-closed at generation (scripts/extensions/agent-binding-kinds.mjsetc.), emitted as pure data (agent-bindings.ts,artifact-floor.ts), resolved by neutral host primitives (agent-roles.ts,extension-roles.ts— fail-loud for system-required roles, degrade for optional ones). Runtime-installed packages contribute renderer bindings through the installed-package collector (Source B); roles bind from build-time presence (documented limitation). - Presence-conditional host surfaces: seeds
(
scripts/seed-lib/extension-presence.mjs— skip-with-notice, determinism pinned for both universes), the connectors catalog (derivesprimitiveOverridesfrom manifests),/connectorsreadiness (generated loader maps).
How extensions reach host capability now (the end-state — the former “one standing non-zero floor” register entry is retired; the owner ruled zero-floor in both directions and the staged work delivered it): the SANCTIONED extension→host paths, each grant- or contract-guarded:
register(ctx)host ports (ExtensionHostContext,packages/sdk-extensions/src/host-context.ts):authSession,jobs,nango,capabilities,mcp, … — grant-gated by manifestrequestedHostPorts.- Per-concern
@cinatra-ai/host:*services published at boot bysrc/lib/register-host-connector-services.ts(SDK contract types inpackages/sdk-extensions/src/host-connector-services-contract.ts, publication asserted member-by-member bysrc/lib/__tests__/host-connector-services-publication.test.ts), consumed through each connector’sdeps.tsslot (namespaced+versionedglobalThisSymbol, bound lazily and fail-loud by the serverEntryregister(ctx); SDK imports in serverEntry graphs stay type-only — held at 0 byhost-peer-value-import-ban). Connectors keep STRUCTURAL local types, so no SDK type import is needed to compile against an older host. - Frozen pure-data contract ids (queue names, config keys): inlined
connector-local constants documented as serialization contracts (e.g. the
twenty-pointer-repairjob name), with the host registry (BACKGROUND_JOB_NAMES, …) staying the single authority.
Scanner correctness (historical)
Section titled “Scanner correctness (historical)”The instance-coupling scanner previously stripped comments with a regex pair
that was not lexical-context aware; the shared single-pass lexer
scripts/audit/lib/strip-comments.mjs fixed two failure classes that hid
real references (a /* inside a line comment swallowing following code; a
// inside a string swallowing the line). The recomputed baseline after the
fix was a one-time RISE sanctioned by bumping SCANNER_EPOCH 1 → 2 in the
same PR. The zero-tolerance flip FROZE the epoch at 2 and retired the
growth allowance; the zero-floor flip pinned the empty baseline, so the
epoch survives purely as a tamper check on the committed baseline document.
Reproduction
Section titled “Reproduction”# end-state (all should pass / print the pinned floors above)node scripts/audit/core-extension-instance-coupling-ban.mjsnode scripts/audit/core-extension-import-ban.mjsnode scripts/audit/discovery-dispatcher-bypass-ban.mjsnode scripts/audit/extension-import-ban.mjs --strict-sdk-onlynode scripts/audit/host-peer-value-import-ban.mjsnode scripts/audit/required-extensions-cover-host-imports.mjs # 8 == 8 == 8node scripts/extensions/generate-extension-manifest.mjs --check # fail-closed integrity of the exempt generated tree
# with the CI monotonic base-ref tamper checksCORE_EXT_INSTANCE_BAN_BASE=origin/main node scripts/audit/core-extension-instance-coupling-ban.mjsCORE_EXT_BAN_BASE=origin/main node scripts/audit/core-extension-import-ban.mjsDISCOVERY_BYPASS_BASE=origin/main node scripts/audit/discovery-dispatcher-bypass-ban.mjsIMPORT_BAN_BASE=origin/main node scripts/audit/extension-import-ban.mjs --strict-sdk-only
# regenerating a pinned-empty baseline REFUSES non-empty outputnode scripts/audit/core-extension-instance-coupling-ban.mjs --write-baselinenode scripts/audit/core-extension-import-ban.mjs --write-baselinenode scripts/audit/extension-import-ban.mjs --write-baselineThe extension source tree must be cloned back first
(node scripts/ci/sync-dev-extensions.mjs) or the gates fail closed.
Docs content licensed under CC-BY-4.0; embedded code snippets under Apache-2.0.