Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .changeset/plugin-manifest-dead-containers-retired.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
---
"@objectstack/spec": minor
---

feat(spec)!: retire the plugin manifest's three dead top-level containers — `capabilities`, `configuration`, `extensions` (#11332, ADR-0049)

<!-- adr-0087: registered plugin-manifest-dead-containers-retired -->

**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
launch-window convention ships it as `minor`; the prescriptions are registered
under protocol major 18 — `RETIRED_KEYS_BY_MAJOR[18]` + the D3 semantic entry
`plugin-manifest-dead-containers-retired` — where `os migrate meta` users will
look).

The census (#11332, cloud leg #12400) measured ZERO reads of each container
itself across objectstack, objectui and cloud, with positive controls — which
settles every key beneath them at once, because a key cannot be read if the
object holding it never is. `configuration.properties.secret` is why this is
false compliance rather than tidying: its describe() promised "value is
encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even
parsed the flag — `secret: true` next to an API key got exactly the same
handling as `secret: false`. `capabilities`' describe() sold "interoperability
and automatic discovery" no discovery path ever performed, and `extensions`
was an untyped `z.record(z.string(), z.unknown())` catch-all nothing
consulted.

FROM → TO:

- `manifest.capabilities` (`implements` / `provides` / `requires` /
`extensionPoints` / `extensions`) → *(removed — no replacement block)*.
Real dependency resolution runs off top-level `manifest.dependencies`,
which stays unchanged. `PluginCapabilityManifestSchema` itself stays
exported: the plugin-registry surface (`plugin-registry.zod.ts`) still
declares it.
- `manifest.configuration` (`{ title, properties }`) → pass options to the
plugin's constructor in `defineStack({ plugins: [new MyPlugin({ … })] })` —
the channel hosts already use, and the only one anything reads.
- `manifest.extensions` → the enforced extension channels:
`contributes.kinds` registers metadata kinds, `navigationContributions`
(ADR-0029 D7) injects navigation into other packages' apps, and code-level
extension lives in the plugin itself (`init`/`start`).

One-line fix: delete the keys (they configured nothing); if you passed
settings via `configuration`, move them to the constructor options your host
already hands the plugin.

The retirement kit:

- `retiredKey()` tombstones on all three keys (`ManifestSchema` is not
`.strict()`, so a plain deletion would silently strip the key — the
`manifest.loading` precedent): authoring one is a `tsc` error (input typed
`never`) and a parse error carrying the prescription
- `kernel/Manifest:capabilities` / `kernel/Manifest:configuration` /
`kernel/Manifest:extensions` in `RETIRED_KEYS_BY_MAJOR[18]`, plus the D3
semantic entry `plugin-manifest-dead-containers-retired`; deliberately NO
D2 conversion — a package manifest is not a stack collection member
(`PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry), so a conversion
would be a transform with no seam that ever runs (the
`kernel/Manifest:loading` reasoning)
- pin tests (`kernel/manifest.test.ts`): per-container rejection carrying the
prescription as the specific zod issue; live neighbours
(`dependencies`, `contributes.kinds`, `navigationContributions`) pinned
green
- the two in-repo authors (driver-memory and plugin-hono-server, each
writing `configuration` + `capabilities` blocks nothing read) stop
authoring the keys; liveness ledger rows move to the tombstone
disposition
18 changes: 11 additions & 7 deletions content/docs/protocol/kernel/plugin-spec.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,7 @@ A **plugin** is the unit of distribution in ObjectStack. It packages ObjectQL sc

## Plugin Manifest

Every plugin **must** have a manifest file that declares its identity, dependencies, and capabilities.
Every plugin **must** have a manifest file that declares its identity, dependencies, and contributions.

### Manifest Location

Expand DownExpand Up@@ -170,17 +170,21 @@ export default definePlugin({
(`packages/spec/src/kernel/manifest.zod.ts`): `id`, `version`, `type` and `name` are
required; the optional fields are `namespace`, `defaultDatasource`, `scope`,
`description`, `permissions`, `objects`, `datasources`, `dependencies`,
`configuration`, `contributes`, `data`, `capabilities`, `extensions`,
`navigationContributions`, `loading`, `engine`, `engines`, `runtime`, `packaging`
and `integrity`.
`contributes`, `data`, `navigationContributions`, `engine`, `engines`, `runtime`,
`packaging` and `integrity`. The `configuration`, `capabilities`, `extensions` and
`loading` keys are **retired tombstones** (ADR-0049 enforce-or-remove): nothing ever
read any of them, so authoring one is now a compile error and a parse error carrying
the migration prescription.

The `displayName` / `author` / `license` / `homepage` / `optionalDependencies` /
`peerDependencies` / `metadata` / `config` / `marketplace` keys above are
**proposal-only** — the schema declares none of them. Notably: compatibility ranges
live in `engines: { platform, protocol }` (or the legacy `engine: { objectstack }`),
not `marketplace.compatibility`; config defaults live in
`configuration: { title, properties }` (a simplified JSON-Schema map with a per-key
`secret` flag), not `config.defaults` / `config.secrets`; and metadata globs are the
not `marketplace.compatibility`; plugin settings are **not** declared in the manifest
at all — the retired `configuration` block (whose per-key `secret` flag promised
encryption/masking that nothing implemented) configured nothing, and a plugin is
configured by the host that composes it, via the options object passed to its
constructor in `defineStack({ plugins: [...] })`; and metadata globs are the
top-level `objects` / `datasources` arrays, not a `metadata` block.
</Callout>

Expand Down
18 changes: 9 additions & 9 deletions content/docs/references/api/package-api.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -199,11 +199,11 @@ Install package request
| **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files |
| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions |
| **dependencies** | `Record<string, string>` | optional | Package dependencies |
| **configuration** | `{ title?: string; properties: Record<string, object> }` | optional | Plugin configuration settings |
| **configuration** | `never` | optional | [REMOVED] `manifest.configuration` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read the block: no settings UI rendered it and no loader resolved a setting from it, so authoring it configured nothing. Worse, `properties.*.secret` promised "value is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even parsed the flag — a false assurance about credential handling. Delete the key. A plugin is configured by the host that composes it: pass options to its constructor in `defineStack({ plugins: [new MyPlugin({ … })] })`, which is the enforced channel. A declarative settings surface must be designed with an enforcing reader first, not revived here. |
| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) |
| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability |
| **extensions** | `Record<string, any>` | optional | Extension points and contributions |
| **capabilities** | `never` | optional | [REMOVED] `manifest.capabilities` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no discovery path ever consulted the block: nothing read `implements`, `provides`, `requires`, `extensionPoints` or `extensions`, so the declared "interoperability and automatic discovery" never happened. Delete the key. Real dependency resolution runs off top-level `manifest.dependencies`, which stays. Capability-based discovery must be designed with an enforcing reader first, not revived here. |
| **extensions** | `never` | optional | [REMOVED] `manifest.extensions` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: `contributes.kinds` registers metadata kinds, `navigationContributions` injects navigation into other packages' apps, and code-level extension happens in the plugin itself (`init`/`start`). |
| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +8 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. |
| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) |
Expand DownExpand Up@@ -321,11 +321,11 @@ Upgrade package request
| **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files |
| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions |
| **dependencies** | `Record<string, string>` | optional | Package dependencies |
| **configuration** | `{ title?: string; properties: Record<string, object> }` | optional | Plugin configuration settings |
| **configuration** | `never` | optional | [REMOVED] `manifest.configuration` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read the block: no settings UI rendered it and no loader resolved a setting from it, so authoring it configured nothing. Worse, `properties.*.secret` promised "value is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even parsed the flag — a false assurance about credential handling. Delete the key. A plugin is configured by the host that composes it: pass options to its constructor in `defineStack({ plugins: [new MyPlugin({ … })] })`, which is the enforced channel. A declarative settings surface must be designed with an enforcing reader first, not revived here. |
| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) |
| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability |
| **extensions** | `Record<string, any>` | optional | Extension points and contributions |
| **capabilities** | `never` | optional | [REMOVED] `manifest.capabilities` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no discovery path ever consulted the block: nothing read `implements`, `provides`, `requires`, `extensionPoints` or `extensions`, so the declared "interoperability and automatic discovery" never happened. Delete the key. Real dependency resolution runs off top-level `manifest.dependencies`, which stays. Capability-based discovery must be designed with an enforcing reader first, not revived here. |
| **extensions** | `never` | optional | [REMOVED] `manifest.extensions` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: `contributes.kinds` registers metadata kinds, `navigationContributions` injects navigation into other packages' apps, and code-level extension happens in the plugin itself (`init`/`start`). |
| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +8 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. |
| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) |
Expand DownExpand Up@@ -405,11 +405,11 @@ Resolve dependencies request
| **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files |
| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions |
| **dependencies** | `Record<string, string>` | optional | Package dependencies |
| **configuration** | `{ title?: string; properties: Record<string, object> }` | optional | Plugin configuration settings |
| **configuration** | `never` | optional | [REMOVED] `manifest.configuration` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read the block: no settings UI rendered it and no loader resolved a setting from it, so authoring it configured nothing. Worse, `properties.*.secret` promised "value is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even parsed the flag — a false assurance about credential handling. Delete the key. A plugin is configured by the host that composes it: pass options to its constructor in `defineStack({ plugins: [new MyPlugin({ … })] })`, which is the enforced channel. A declarative settings surface must be designed with an enforcing reader first, not revived here. |
| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) |
| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability |
| **extensions** | `Record<string, any>` | optional | Extension points and contributions |
| **capabilities** | `never` | optional | [REMOVED] `manifest.capabilities` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no discovery path ever consulted the block: nothing read `implements`, `provides`, `requires`, `extensionPoints` or `extensions`, so the declared "interoperability and automatic discovery" never happened. Delete the key. Real dependency resolution runs off top-level `manifest.dependencies`, which stays. Capability-based discovery must be designed with an enforcing reader first, not revived here. |
| **extensions** | `never` | optional | [REMOVED] `manifest.extensions` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: `contributes.kinds` registers metadata kinds, `navigationContributions` injects navigation into other packages' apps, and code-level extension happens in the plugin itself (`init`/`start`). |
| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +8 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. |
| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) |
Expand Down
Loading
Loading