Extension boundaries
2server is an independent repository. Its AGENTS.md defines contributor rules; this document defines shared contributions and retained compatibility contracts. Product documentation, schemas and tests must work from a standalone checkout. Consuming repositories own live manifests and operational evidence.
Ownership
Core owns argument handling, explicit composition, validation of shared contracts, instance binding, resource ownership, locking, persistence and shared deploy engines. Templates own service/provider semantics, custom validation, commands, health/backup behavior and extension-specific state. See CLI architecture and authoring.
A YAML-only Service recipe needs no core changes. Native hooks and external
runtime adapters need one registration in
src/modules/extensions/application/registry.ts. That composition is deliberate;
the source dispatcher and VM lifecycle must not gain an extension-name branch.
These extensions ship in the CLI package and share infrastructure helpers. They
are not independently installed or sandboxed plugins.
Contribution contract
Optional fields in ExtensionHooks/Extension serve the existing shared use cases:
summary(config, state): safe operator messages; print references/paths, never credential values. Named monitoring instances report their own URL and path.diagnostics(config): read-only shell fragments consumed by diagnostic output; use bound instance identity and normal shell quoting.backupStoragePermissions(config): object-admin permission intent. PostgreSQL requests it for pgBackRest; dump backups do not request it. Core computes shared provisioning intent without inspecting a provider-specific spec.alertRules: static Prometheus groups supplied once per template, including groups needed for retained metrics from disabled or retired workloads.controlState: declared roots and a strict schema for portable paths. Control composes them with its own certificate/Compose/history allowlist. Roots and paths must be relative, bounded and free of traversal; undeclared files and symlinks remain refused. Deactivating an instance does not discard its saved credentials. State remains private and subject to snapshot size limits.
Configuration-dependent hooks are wrapped by bindInstance. Static catalog
contributions are collected once per template. Keep new capabilities tied to a
real shared use case; do not add a generic hook execution language.
Frozen compatibility contracts
The following remain intentionally supported. They are not the onboarding path for a new extension:
shared/cli/resource-request.tsandcli/resources.tsretain existing grammar, aliases and routing forpostgres,monitor,webhook, andrecovery.extensions/cli/legacy.tsexplicitly composes only template-owned legacy adapters. New commands use the YAML command map andapp NAMEloader.- Legacy keys (
postgres,redis,nats,monitoring,imageProxy,alertWebhookEnv,webhooks) and exported registry aliases keep old manifests working. Their presence is not permission to put new service logic in config. - Monitoring's
monitoring-credentials.jsonandmonitoring/NAME/credentials.jsonpaths remain unchanged. The template now declares both; capture and snapshot validation share the same policy. - GCS backup Terraform input keeps the
pgbackrest_enabledname and the legacy backup-storage result keeps its/postgresdestination suffix. The permission decision belongs to the capability, and templates choose their own object prefixes using the shared bucket identity. This review performs no state or Terraform migration. - Shared output protocols such as
redis,natsandpostgresqlare connection vocabulary, rather than runtime dispatch identities.
Do not remove these contracts during an isolation cleanup. A breaking migration requires a separately described migration and acceptance checks.
Enforcement and evidence
tests/architecture.test.ts rejects template implementation imports outside
registry/legacy composition, sibling-template imports, core reads of built-in
template config fields, runtime identity dispatch, domain effects and shared
feature dependencies. The existing generic command loader checks the catalog's
command allowlist before dynamic import.
tests/extension-contributions.test.ts covers bound summaries and diagnostics,
backup permission intent, third-template alert/state contributions, retained
legacy credentials, undeclared paths, traversal and symlink rejection.
tests/external-extension-runtime.test.ts registers another runtime and exercises
source dispatch without changing core, including rejected VM operations.
Run bun run check and the artifact checks after changing these
contracts. The default suite uses isolated fixtures/mocks; local passing tests
do not establish live IAM, DNS, email delivery or production restore health.
