From 15963192715b505a2b2a82c9430e683ee0a80a47 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 13:51:04 +0000 Subject: [PATCH] docs(spec): manifest.runtime trust-tier text states publish-gate-only enforcement truthfully MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `manifest.loading` tombstone redirected upgrading authors to the plugin trust tier as something "which are enforced". Measured, nothing in this repo dispatches on the tier — its only reads are two CLI progress lines that echo the value — so the redirection pointed at a key with no local enforcement: ADR-0049 false compliance with a shipped migration message attached. The truthful statement is a split, now carried in all four places the claim ships (tombstone prescription, PluginRuntime enum describe, manifest.runtime field describe, ADR-0087 D3 entry -> protocol-upgrade-guide): - enforced at the cloud marketplace publish gate (unverified publisher requesting the `node` tier -> HTTP 422 + forced manual review), which is why the key is not retired; - not enforced at load, so a locally installed plugin is not isolated by the tier it declares. Text only: no schema shape and no accept/reject change (check:authorable-surface green, accept-set pinned). The permissions half of the tombstone sentence is preserved verbatim and rides with the permissions card. Co-Authored-By: Claude --- ...nifest-runtime-trust-tier-truthful-text.md | 51 ++++++ content/docs/references/api/package-api.mdx | 12 +- content/docs/references/api/protocol.mdx | 4 +- content/docs/references/kernel/manifest.mdx | 6 +- .../references/kernel/package-registry.mdx | 8 +- .../references/kernel/package-upgrade.mdx | 8 +- docs/protocol-upgrade-guide.md | 2 +- packages/spec/liveness/README.md | 2 +- packages/spec/liveness/manifest.json | 2 +- packages/spec/spec-changes.json | 4 +- packages/spec/src/kernel/manifest.zod.ts | 52 +++++- .../plugin-runtime-tier-truthful-text.test.ts | 149 ++++++++++++++++++ .../17.plugin-manifest-loading-retired.ts | 8 +- packages/spec/src/migrations/registry.ts | 8 +- 14 files changed, 282 insertions(+), 34 deletions(-) create mode 100644 .changeset/manifest-runtime-trust-tier-truthful-text.md create mode 100644 packages/spec/src/kernel/plugin-runtime-tier-truthful-text.test.ts diff --git a/.changeset/manifest-runtime-trust-tier-truthful-text.md b/.changeset/manifest-runtime-trust-tier-truthful-text.md new file mode 100644 index 0000000000..133476ed76 --- /dev/null +++ b/.changeset/manifest-runtime-trust-tier-truthful-text.md @@ -0,0 +1,51 @@ +--- +"@objectstack/spec": patch +--- + +docs(spec): `manifest.runtime` trust-tier text states publish-gate-only enforcement truthfully (#11330) + +Text-only correction on a published spec surface. **No schema shape, no +accept/reject behaviour, and no generated accept-set changes** — `runtime` still +accepts `node` / `sandbox` / `worker` and still may be unset, pinned in +`plugin-runtime-tier-truthful-text.test.ts` and confirmed by a green +`check:authorable-surface` (the authorable-surface products are byte-identical). + +`manifest.loading`'s tombstone told every upgrading author, verbatim, that +`loading.sandboxing` "never isolated anything … 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**". Measured, that redirection +pointed at a key this repo does not enforce at all: nothing dispatches on the +tier — its only reads are two CLI progress lines that echo the value, and the +QuickJS runner under `packages/runtime/src/sandbox/` is the hook/action +*script-body* sandbox, never reached from a plugin's declared tier. An author who +followed the prescription got the same nothing they were being warned about, one +key over: ADR-0049 false compliance with a shipped migration message attached. + +The honest statement is a **split**, and the corrected text now carries both +halves everywhere the claim ships: + +- **Enforced at the cloud marketplace publish gate** — an unverified publisher + requesting the `node` tier is hard-rejected (HTTP 422) and forced to manual + review. That gate is a real consumer, which is why the key is **not** retired. +- **Not enforced at load** — load-side enforcement is not implemented, so a + locally installed plugin is not isolated by the tier it declares. `sandbox` and + `worker` name the isolation the publish gate assumes, not isolation the loader + applies. + +Corrected in all four places the claim is published: the `loading` tombstone +prescription, the `PluginRuntime` enum describe, the `manifest.runtime` field +describe, and the ADR-0087 D3 entry `plugin-manifest-loading-retired` (which +renders into `docs/protocol-upgrade-guide.md` — fixing only the schema would have +left the upgrade guide still calling the tier a surface "the platform actually +enforces"). The `packages/spec/liveness/` ledger row and README narrative are +brought to the same reading. + +Maintainer ruling 2026-08-30 (option **B now**): say it truthfully first. +Enforcing the tier at load is option **A**, tracked as a v18 direction and +deliberately not built here. Retirement (option **C**) was ruled out. ⚠️ The +**permissions** half of that same tombstone sentence — "and the permission +declarations, which are enforced" — is preserved **verbatim** and rides with +#11333 per the ruling's coordination pin; a pin test asserts it is untouched so +the handoff is visible rather than silent. + + diff --git a/content/docs/references/api/package-api.mdx b/content/docs/references/api/package-api.mdx index efe1a61b11..ce9256bfad 100644 --- a/content/docs/references/api/package-api.mdx +++ b/content/docs/references/api/package-api.mdx @@ -205,10 +205,10 @@ Install package request | **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. | +| **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 — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. Use the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | | **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | -| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares | | **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | | **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | @@ -327,10 +327,10 @@ Upgrade package request | **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. | +| **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 — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. Use the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | | **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | -| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares | | **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | | **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | @@ -411,10 +411,10 @@ Resolve dependencies request | **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. | +| **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 — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. Use the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | | **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | -| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares | | **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | | **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index e509fd154d..7ee8e200e2 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -1845,10 +1845,10 @@ Install package request | **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. | +| **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 — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. Use the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | | **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | -| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares | | **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | | **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | diff --git a/content/docs/references/kernel/manifest.mdx b/content/docs/references/kernel/manifest.mdx index d92c24a58c..27984c7743 100644 --- a/content/docs/references/kernel/manifest.mdx +++ b/content/docs/references/kernel/manifest.mdx @@ -45,10 +45,10 @@ const result = ManifestSchema.parse(data); | **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. | +| **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 — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. Use the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | | **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | -| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares | | **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | | **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | @@ -206,7 +206,7 @@ Structured plugin permission grants (ADR-0025 §3.2) ## PluginRuntime -Plugin trust tier (ADR-0025 §3.6) +Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (an unverified publisher requesting `node` is rejected with HTTP 422 and forced to manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares ### Allowed Values diff --git a/content/docs/references/kernel/package-registry.mdx b/content/docs/references/kernel/package-registry.mdx index 8821bc04bd..0cb9c50214 100644 --- a/content/docs/references/kernel/package-registry.mdx +++ b/content/docs/references/kernel/package-registry.mdx @@ -209,10 +209,10 @@ Install package request | **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. | +| **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 — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. Use the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | | **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | -| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares | | **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | | **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | @@ -304,10 +304,10 @@ Installed package with runtime lifecycle state | **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. | +| **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 — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. Use the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | | **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | -| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares | | **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | | **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | diff --git a/content/docs/references/kernel/package-upgrade.mdx b/content/docs/references/kernel/package-upgrade.mdx index 1b53cfc8aa..d65043fd6a 100644 --- a/content/docs/references/kernel/package-upgrade.mdx +++ b/content/docs/references/kernel/package-upgrade.mdx @@ -158,10 +158,10 @@ Upgrade package request | **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. | +| **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 — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. Use the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | | **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | -| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares | | **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | | **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | @@ -300,10 +300,10 @@ Pre-upgrade state snapshot for rollback capability | **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. | +| **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 — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. Use the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | | **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | -| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier (ADR-0025 §3.6) | +| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares | | **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) | | **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) | diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index c756598277..6fdc8975e7 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -493,7 +493,7 @@ This is a RUNTIME registration API, not stored metadata, so — like `hook-conte - **`plugin-activation-events-retired`** — `kernel.dynamicLoadRequest.activationEvents / studio.studioPluginManifest.activationEvents` → (removed — delete the key. Every plugin activates immediately on load/registration, which is the only behaviour that has ever existed; `activate()` still runs at registration time. Lazy activation, if built, returns via the enforce route of ADR-0049 through a new ADR, with a vocabulary its executor actually honours) - Why not automatic: Both `activationEvents` keys — and the `ActivationEventSchema` trigger vocabulary they embedded (`onCommand` / `onRoute` / … / `onView` after the #4653 convergence) — promised lazy plugin activation ("plugins remain dormant until an activation event fires") that no runtime in objectstack, cloud, cloud-v1 or objectui ever implemented: nothing anywhere read the key, every plugin activates immediately, and cloud-v1's own ROADMAP recorded lazy activation as unimplemented (planned v0.4.0). That is the ADR-0049 false-compliance shape in the semantically-lying direction: an author writing `activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }]` expected deferral and got eager activation with a clean parse. Neither parent shape is stored metadata — `StudioPluginManifest` is TS configuration parsed by `defineStudioPlugin` (a root schema, never part of a stack tree) and `DynamicLoadRequest` is a runtime request shape with no caller — so no `sys_metadata` row can carry the key and there is no source for the D2 chain to rewrite; this entry is the D3 record. The kernel key is tombstoned via `retiredKey()` (its schema is not `.strict()`; a plain delete would strip an authored value silently), the studio key is rejected by the strict manifest parse with a guidance prescription (as are its former VS Code-flavoured aliases `activation` / `events` / `onActivate`), and the orphaned `ActivationEventSchema` / `ActivationEvent` exports are removed from `./kernel` and `./studio` with the keys (#3950: an exported schema with no consumer is read as a capability). #4657. SUPERSEDED ON THE KERNEL SIDE by #4834 (same unreleased major): the whole `DynamicLoadRequest` shape — and the rest of the plugin-runtime family with it — was removed, which took this key's `retiredKey()` tombstone with it. That is strictly stronger than the tombstone, not weaker: there is no longer a `DynamicLoadRequest` to author the key INTO, so the prescription an author needs is no longer "delete this key" but "this request shape does not exist" (see `plugin-runtime-family-retired`). The studio half of this entry is unaffected and still enforced by the strict manifest parse. - Done when: No `defineStudioPlugin` input authors `activationEvents` — authoring it is an unknown key on the strict studio manifest and a parse error carrying the prescription. On the kernel side the stronger #4834 criterion applies instead: there is no `DynamicLoadRequest` type or schema left to author it into at all. No code imports `ActivationEventSchema` / `ActivationEvent` from `@objectstack/spec/kernel` or `@objectstack/spec/studio` (TS2305 after upgrade). Runtime behaviour is byte-identical: plugins loaded eagerly before and after. -- **`plugin-manifest-loading-retired`** — `manifest.loading (the whole block: strategy / preload / codeSplitting / dynamicImport / initialization / dependencyResolution / hotReload / caching / sandboxing / monitoring)` → nothing to re-declare — 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` in `packages/core/src/plugin-order.ts`). For the isolation `loading.sandboxing` appeared to configure, use the plugin trust tier (`manifest.runtime`, ADR-0025 §3.6) and the manifest permission declarations, which are the surfaces the platform actually enforces +- **`plugin-manifest-loading-retired`** — `manifest.loading (the whole block: strategy / preload / codeSplitting / dynamicImport / initialization / dependencyResolution / hotReload / caching / sandboxing / monitoring)` → nothing to re-declare — 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` in `packages/core/src/plugin-order.ts`). For the isolation `loading.sandboxing` appeared to configure, note that the plugin trust tier (`manifest.runtime`, ADR-0025 §3.6) does not supply it either: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. Use the manifest permission declarations, which are the surfaces the platform actually enforces - Why not automatic: ADR-0049 enforce-or-remove; maintainer ruling 2026-08-04 on #4914. The block declared a complete plugin loading policy and NOTHING read it. A bare-name scan of all three repos — objectstack, cloud (measured 2026-08-09) and objectui (measured at pickup), each with a control probe proving the scan saw the tree — put every hit inside `packages/spec` itself: this module's own declaration, its own unit tests, the `Manifest.loading` embed and the generated artifacts. `manifest.loading.*` had zero readers in `packages/core`, `packages/runtime` and `packages/metadata`. So the key parsed, entered the manifest, and changed nothing — #3950, at the scale of a whole block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared process / vm / iframe / web-worker isolation, IPC transports and an `allowedServices` ACL, so an AI author (ADR-0033) reading that vocabulary concluded the platform isolates plugins, wrote the config, and received a clean parse and zero isolation. An inert security control is worse than an absent one because it is believed. Hot reload was additionally a TWO-SOURCE defect: the docs pointed at this dead `PluginHotReloadSchema` while the only implementation body, `HotReloadManager` (`packages/core/src/hot-reload.ts`), reads a different vocabulary — `HotReloadConfigSchema` in `plugin-lifecycle-advanced.zod.ts`. Ruling §2 converges on the surviving side: that schema is KEPT as the starting point for a future enforce decision (it has an implementation body but no runtime composes it yet), and enforcing it is deliberately a separate decision, not this retirement. Why D3 semantic and not a D2 conversion: the chain walks a normalized STACK and `applyConversionsToStoredItem` maps a metadata type onto one of its collections. A package manifest is neither — `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry, so a manifest is not a stack collection member and a stored manifest row passes that seam through unchanged. A conversion would be a transform with no seam that ever runs. - Done when: No `objectstack.plugin.json` and no stored package manifest carries a `loading` key. The enforced channel is the one place a manifest is parsed with an author present: `os plugin build` runs `ManifestSchema.safeParse` and exits non-zero, printing the tombstone prescription, so a manifest still declaring `loading` fails its build rather than shipping. TypeScript authors get it earlier still — `loading` is typed `never`, so assigning it is a `tsc` error. ⚠️ Runtime behaviour is deliberately UNCHANGED and must be verified as such: nothing ever read the block, so removing it removes no behaviour. A package ALREADY INSTALLED whose stored manifest carries `loading` keeps working — the registry's `validate()` is an explicit diagnostic and not a gate (it catches, logs `[metadata_spec_invalid]`, and registers the item anyway, deliberately, so bad metadata is never a data outage), so such a row degrades to one log line at registration rather than a boot failure. Clear it by deleting the key from the source manifest and reinstalling. - **`plugin-runtime-family-retired`** — `kernel.dynamicLoadRequest / kernel.dynamicUnloadRequest / kernel.dynamicPluginResult / kernel.pluginSource / kernel.dynamicPluginOperation` → (removed — there is no replacement shape, because there is no operation to describe. Plugins are composed at boot: `defineStack` registers them and the kernel runs register → init → start; the set is fixed until the process restarts. Delete the import and the value. Runtime plugin loading, if it is ever built, returns via the enforce route of ADR-0049 through a new ADR — loader first, vocabulary second) diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 9422b50402..0813e7994d 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -839,7 +839,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | validation | seeded 2026-08-01 (#4488). The ADR-0020 carrier: the evaluator honors active/events/priority/severity/type/condition/message (the zod header's "only reads type/condition/…" prose is STALE — trust the ledger). Dead 3 = label/description/tags, declared governance metadata, kept unmarked. Union walk boundary recorded: only base + `script` keys walked; per-variant keys are governed by the evaluator's tests, not ledger rows. **No longer a registered metadata kind** — #4509 retired it under ADR-0088 (a standalone rule had no object-binding key and every variant is `.strict()`, so it bound to nothing and gated no write; a state machine authored that way saved cleanly and did nothing). The rule VOCABULARY is untouched and fully live via `object.validations[]`, so the ledger keeps governing it through the gate's spec-only override, alongside `webhook` and `query`. The contrast with the two bridges in the same batch is the point: enforce-or-remove picked ENFORCE where the feature existed and only the wiring was missing, and REMOVE where the shape itself could not carry the feature | | api | seeded 2026-08-04 (#5271, part of #5206; PR #5312) — **not a metadata type until that same change made it one**, which is the row's point: governance and registration landed together, the treatment `datasource` did not get (#4487) and paid for with six inert keys found by hand. What #5206 measured before the fix: `api` was in neither `DEFAULT_METADATA_TYPE_REGISTRY` nor `BUILTIN_METADATA_TYPE_SCHEMAS`, so `saveMetaItem`'s `resolveOverlaySchema('api', …)` → `getMetadataTypeSchema('api')` returned `undefined` and took its own documented branch — an unregistered type is stored **unvalidated** — while `getMetaTypes()` could not enumerate the type at all, so Studio rendered neither list nor form. That issue names the shape precisely and it is the inverse of this ledger's usual one: **enforced but undeclared** (the matcher was already indexing these entries, #5089), where `dead` is declared-but-unenforced. The seeding pass classified 27 keys — live 25 / planned 2 / dead 0 — each cited `file:line` at the consumer layer that reads it: the MATCHER (`packages/metadata/src/endpoint-matcher.ts`) indexes `name`/`path`/`method`; the EXECUTOR (`packages/runtime/src/endpoint-executor.ts`) dispatches on `type` and reads `target`/`objectParams`; the POLICY chain (`packages/runtime/src/endpoint-policy.ts` + `security/inbound-rate-limit.ts`) enforces `authRequired`/`rateLimit`/`cacheTtl`; the MAPPING layer (`packages/runtime/src/api-mapping.ts`) applies `inputMapping`/`outputMapping`; and OpenAPI enrichment (`packages/rest/src/openapi-endpoints.ts`) emits `summary`/`description`. Timing was the reason it was cheap: #5040's E-series had built every one of those consumers and all of it was on main, so each key had a real evidence path rather than a promise. **Planned 2 = `inputMapping.transform` + `outputMapping.transform`, and `planned` rather than `dead` is load-bearing**: `dead` here means parsed with no consumer — a silent no-op — and these are the opposite, parsed and then LOUDLY REFUSED at publish (`endpoint-publish-gate.ts` mappingGate) and again at runtime, because no transformation-function registry exists anywhere in the platform. An author who writes one is told so and told what to do instead, so there is nothing for enforce-or-remove to chase; they stay in the vocabulary because admitting them needs a function registry **and** a sandbox ruling (#5040 §3.4), which is a design decision, not a key to quietly delete. Zero dead | | capability | seeded 2026-08-08 (#5961; PR #6540) — `CapabilityDeclarationSchema`, the DECLARATION side of ADR-0066 D1's three-way separation: packages DEFINE a capability, permission sets GRANT it via `systemPermissions`, resources REQUIRE it via `requiredPermissions`. **The gate's 12 and the seeding PR's 5 are the same measurement at two granularities** — PR #6540 call-graph-closed **5 authorable properties**, every one to a real reader in `packages/plugins/plugin-security/src/bootstrap-declared-capabilities.ts` (the one consumer that turns a declaration into a `sys_capability` row), all `live`, with no `PENDING_GOVERNANCE` debt recorded; the other 7 are the ADR-0010 protection-envelope keys the gate auto-classifies `live` and which carry `null` verdicts in the file, exactly as on `permission`/`position`. The same worked example as `api` above and PR #6540 says so in those words — **enforced but undeclared**, the mirror of the hole #5271 closed. What #5961 measured: absent from `DEFAULT_METADATA_TYPE_REGISTRY`, `BUILTIN_METADATA_TYPE_SCHEMAS` and `HAND_CRAFTED_SCHEMAS`, so `isRuntimeCreateAllowed()` took its no-static-entry fallback (permanently true) and `saveMetaItem` its no-schema branch — `PUT /api/v1/meta/capability/:name` accepted **arbitrary JSON** onto an authorization surface whose names `systemPermissions`/`requiredPermissions` resolve by string, while `/meta/types` synthesised a false `allowRuntimeCreate: true` descriptor Studio drew a raw-JSON create form from. #5870 did not open that path (the write gate reads the registry, not the item store); it only made the type visible in `getMetaTypes()`, and both the issue and this row say so to stop the next reader filing it as a regression. Landed as ruling A on ADR-0066 D1's own authority: `allowRuntimeCreate: false` **and** `allowOrgOverride: false`, the second self-judged inside the ruling's rationale and flagged for veto — a tenant overlay of a package declaration would lift `scope` from `org` to `platform`, which is the one field on this type that is an escalation rather than display. Its reverse verification is worth copying: deleting the registry entry gave 7 red / 3 green and measured something **sharper than predicted** — a garbage payload turned 422 rather than resolving, i.e. the schema binding is a real second line of defence behind the registry row, not a restatement of it; deleting the schema binding alone gave exactly 3 red. `packageId` is the one key that reads oddly: deliberately a FALLBACK, not the primary, since #5870 added `capabilities` to the ObjectQL stamped-collection list so `_packageId` now reaches a declaration and wins — it stays `live` because the fallback branch still decides materialization for any declaration arriving unstamped. Zero dead | -| manifest | seeded 2026-08-23 (#10728) — **not a metadata type and not a stack collection either**, which is the row's whole point. `ManifestSchema` (`packages/spec/src/kernel/manifest.zod.ts:132`) is what an author writes as `objectstack.config.ts` or a packaged manifest; it is parsed at `packages/objectql/src/registry.ts:2950` and by `os plugin build`, and it sat outside the ratchet's universe entirely — `GOVERNED` listed no `plugin`/`manifest`/`package`, `SPEC_ONLY_SCHEMAS` covered only webhook/query/validation/qa, `PENDING_GOVERNANCE` was empty so the gate reported itself **complete**, and `liveness/` had no file for it. A ratchet extended only to unregistered KINDS would not have reached it either: the retired-key entry `17.kernel__Manifest__loading.ts` records that `PLURAL_TO_SINGULAR` has no `packages`/`plugins` entry, so a manifest is never walked as a stack collection member. That blind spot was paid for twice, by hand and after the fact — `loading`'s ten inert keys (#4914, one of them `sandboxing`, which isolated nothing while looking like isolation) and the `contributes` census (#10627), which found exactly ONE reader of the 11-member block monorepo-wide. Dead 21 = the ten dead `contributes` members (`kinds` is the sole live one — `engine.ts:4504` → `registerKind`), the five `capabilities.*` and two `configuration.*` keys (all three containers have ZERO reads of the container itself, so no key beneath one can be read), plus `extensions`, `integrity`, `runtime`, and the tombstoned `loading` whose row must stay because `retiredKey()` keeps the key in the walked shape. **Two of those are security-shaped and neither is retired here**: `integrity` declares per-file digests the runtime is documented to re-verify at unpack while nothing computes or checks them, and `runtime` — the ADR-0025 §3.6 trust tier — is read only by two CLI lines that ECHO the value, with no `runtime === 'sandbox'` branch anywhere, even though `loading`'s own tombstone redirects upgrading authors to it as something "which [is] enforced". The `contributes` dispositions have since MOVED: the cloud leg was measured CLEAN 2026-08-24 (#10812; cloud `origin/main` @ 5b5925a, zero `manifest.contributes` reads, controls held), and #10724 then executed — the nine mechanically-dead members are `retiredKey()` tombstones (D3 `plugin-manifest-contributes-dead-members-retired`), their rows staying because a tombstone keeps the key in the walked shape. #10726 then executed too (ruled B 2026-08-22; D3 `plugin-manifest-contributes-routes-retired`), tombstoning `routes` and leaving `kinds` the block's sole live member. The remaining non-`contributes` `dead` rows keep their three-repo census verdicts as an enforce-or-remove worklist, not a licence to delete | +| manifest | seeded 2026-08-23 (#10728) — **not a metadata type and not a stack collection either**, which is the row's whole point. `ManifestSchema` (`packages/spec/src/kernel/manifest.zod.ts:132`) is what an author writes as `objectstack.config.ts` or a packaged manifest; it is parsed at `packages/objectql/src/registry.ts:2950` and by `os plugin build`, and it sat outside the ratchet's universe entirely — `GOVERNED` listed no `plugin`/`manifest`/`package`, `SPEC_ONLY_SCHEMAS` covered only webhook/query/validation/qa, `PENDING_GOVERNANCE` was empty so the gate reported itself **complete**, and `liveness/` had no file for it. A ratchet extended only to unregistered KINDS would not have reached it either: the retired-key entry `17.kernel__Manifest__loading.ts` records that `PLURAL_TO_SINGULAR` has no `packages`/`plugins` entry, so a manifest is never walked as a stack collection member. That blind spot was paid for twice, by hand and after the fact — `loading`'s ten inert keys (#4914, one of them `sandboxing`, which isolated nothing while looking like isolation) and the `contributes` census (#10627), which found exactly ONE reader of the 11-member block monorepo-wide. Dead 21 = the ten dead `contributes` members (`kinds` is the sole live one — `engine.ts:4504` → `registerKind`), the five `capabilities.*` and two `configuration.*` keys (all three containers have ZERO reads of the container itself, so no key beneath one can be read), plus `extensions`, `integrity`, `runtime`, and the tombstoned `loading` whose row must stay because `retiredKey()` keeps the key in the walked shape. **Two of those are security-shaped and neither is retired here**: `integrity` declares per-file digests the runtime is documented to re-verify at unpack while nothing computes or checks them, and `runtime` — the ADR-0025 §3.6 trust tier — is read only by two CLI lines that ECHO the value, with no `runtime === 'sandbox'` branch anywhere, while `loading`'s own tombstone used to redirect upgrading authors to it as something "which [is] enforced". `runtime`'s disposition has since MOVED and is the family's one SPLIT verdict: #12400 measured the cloud leg and found a real consumer — the marketplace publish gate hard-rejects (HTTP 422) an unverified publisher requesting the `node` tier — so the maintainer ruling of 2026-08-30 (#11330) took "say it truthfully" over retirement, and retirement is ruled OUT because deleting the key would tear out that gate's input. The tombstone, both `runtime` describes and the D3 entry now state the split (publish-gate enforced; load-side NOT implemented); the `dead` row stays, scoped to LOAD-SIDE dispatch in objectstack+objectui, and load-side enforcement is a v18 direction rather than a removal. The `contributes` dispositions have since MOVED: the cloud leg was measured CLEAN 2026-08-24 (#10812; cloud `origin/main` @ 5b5925a, zero `manifest.contributes` reads, controls held), and #10724 then executed — the nine mechanically-dead members are `retiredKey()` tombstones (D3 `plugin-manifest-contributes-dead-members-retired`), their rows staying because a tombstone keeps the key in the walked shape. #10726 then executed too (ruled B 2026-08-22; D3 `plugin-manifest-contributes-routes-retired`), tombstoning `routes` and leaving `kinds` the block's sole live member. The remaining non-`contributes` `dead` rows keep their three-repo census verdicts as an enforce-or-remove worklist, not a licence to delete | The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every misleading entry carries `authorWarn` so authors hear about it at compile time diff --git a/packages/spec/liveness/manifest.json b/packages/spec/liveness/manifest.json index 2c4abb6390..7103f4b52b 100644 --- a/packages/spec/liveness/manifest.json +++ b/packages/spec/liveness/manifest.json @@ -253,7 +253,7 @@ "status": "dead", "verifiedAt": "2026-08-23", "evidenceScope": "cross-repo", - "note": "READ TWICE, ENFORCED NOWHERE — and the two are not the same thing, so read this row before assuming zero references. The only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value: packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered here, and echoing a string into a build log is not isolation. WHY THIS IS SECURITY-SHAPED RATHER THAN COSMETIC: the tombstone this ledger records one key above tells every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" (manifest.zod.ts:527-530, and the same sentence in the shipped ADR-0087 D3 entry). Measured here, that redirection points at a key with no enforcement in this repo. ⚠️ SCOPE LIMIT, deliberately not resolved by this row: the cloud control plane is unmeasured (see `_note`), and plugin trust-tier enforcement is exactly the kind of thing that could live there — cloud mirrors these shapes when it validates a published `.osplugin`. `dead` is therefore the objectstack+objectui verdict and an enforce-or-remove worklist row, NOT a licence to delete: the cloud leg must be measured first, on the #10724 precondition. Filed as #11330 rather than acted on here." + "note": "READ TWICE HERE, ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — three different facts, so read this row before assuming either zero references or zero enforcement. The only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value: packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered here, and echoing a string into a build log is not isolation. WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC, AND WHAT WAS DONE: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce, i.e. ADR-0049 false compliance with a shipped migration message attached. ⚠️ SCOPE LIMIT NOW DISCHARGED: the cloud leg was measured by #12400 and it CHANGED the picture — the cloud control plane really does consume this key at the marketplace publish gate (`plugin-permission-audit.ts`: an unverified publisher requesting the `node` tier is hard-rejected with HTTP 422 and forced to manual review, reading of 2026-08-29). So the honest statement is split: ENFORCED at publish (cloud), NOT ENFORCED at load (here). Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input. That correction has landed: the tombstone and both `runtime` describes now state the publish-gate/load-side split, and so does the ADR-0087 D3 entry `plugin-manifest-loading-retired`. ⚠️ READ `dead` NARROWLY HERE: it is the objectstack+objectui verdict about LOAD-SIDE dispatch (still true — the only local reads are the two CLI echoes above), and it is NOT a licence to delete, because the key has a real cross-repo consumer. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal." }, "packaging": { "status": "live", diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index 693e3e8427..fb6dc458a0 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -854,7 +854,7 @@ }, { "surface": "manifest.loading (the whole block: strategy / preload / codeSplitting / dynamicImport / initialization / dependencyResolution / hotReload / caching / sandboxing / monitoring)", - "replacement": "nothing to re-declare — 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` in `packages/core/src/plugin-order.ts`). For the isolation `loading.sandboxing` appeared to configure, use the plugin trust tier (`manifest.runtime`, ADR-0025 §3.6) and the manifest permission declarations, which are the surfaces the platform actually enforces", + "replacement": "nothing to re-declare — 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` in `packages/core/src/plugin-order.ts`). For the isolation `loading.sandboxing` appeared to configure, note that the plugin trust tier (`manifest.runtime`, ADR-0025 §3.6) does not supply it either: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. Use the manifest permission declarations, which are the surfaces the platform actually enforces", "migrationId": "plugin-manifest-loading-retired", "toMajor": 17, "rationale": "ADR-0049 enforce-or-remove; maintainer ruling 2026-08-04 on #4914. The block declared a complete plugin loading policy and NOTHING read it. A bare-name scan of all three repos — objectstack, cloud (measured 2026-08-09) and objectui (measured at pickup), each with a control probe proving the scan saw the tree — put every hit inside `packages/spec` itself: this module's own declaration, its own unit tests, the `Manifest.loading` embed and the generated artifacts. `manifest.loading.*` had zero readers in `packages/core`, `packages/runtime` and `packages/metadata`. So the key parsed, entered the manifest, and changed nothing — #3950, at the scale of a whole block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared process / vm / iframe / web-worker isolation, IPC transports and an `allowedServices` ACL, so an AI author (ADR-0033) reading that vocabulary concluded the platform isolates plugins, wrote the config, and received a clean parse and zero isolation. An inert security control is worse than an absent one because it is believed. Hot reload was additionally a TWO-SOURCE defect: the docs pointed at this dead `PluginHotReloadSchema` while the only implementation body, `HotReloadManager` (`packages/core/src/hot-reload.ts`), reads a different vocabulary — `HotReloadConfigSchema` in `plugin-lifecycle-advanced.zod.ts`. Ruling §2 converges on the surviving side: that schema is KEPT as the starting point for a future enforce decision (it has an implementation body but no runtime composes it yet), and enforcing it is deliberately a separate decision, not this retirement. Why D3 semantic and not a D2 conversion: the chain walks a normalized STACK and `applyConversionsToStoredItem` maps a metadata type onto one of its collections. A package manifest is neither — `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry, so a manifest is not a stack collection member and a stored manifest row passes that seam through unchanged. A conversion would be a transform with no seam that ever runs." @@ -1939,7 +1939,7 @@ }, { "surface": "manifest.loading (the whole block: strategy / preload / codeSplitting / dynamicImport / initialization / dependencyResolution / hotReload / caching / sandboxing / monitoring)", - "replacement": "nothing to re-declare — 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` in `packages/core/src/plugin-order.ts`). For the isolation `loading.sandboxing` appeared to configure, use the plugin trust tier (`manifest.runtime`, ADR-0025 §3.6) and the manifest permission declarations, which are the surfaces the platform actually enforces", + "replacement": "nothing to re-declare — 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` in `packages/core/src/plugin-order.ts`). For the isolation `loading.sandboxing` appeared to configure, note that the plugin trust tier (`manifest.runtime`, ADR-0025 §3.6) does not supply it either: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. Use the manifest permission declarations, which are the surfaces the platform actually enforces", "migrationId": "plugin-manifest-loading-retired", "toMajor": 17, "rationale": "ADR-0049 enforce-or-remove; maintainer ruling 2026-08-04 on #4914. The block declared a complete plugin loading policy and NOTHING read it. A bare-name scan of all three repos — objectstack, cloud (measured 2026-08-09) and objectui (measured at pickup), each with a control probe proving the scan saw the tree — put every hit inside `packages/spec` itself: this module's own declaration, its own unit tests, the `Manifest.loading` embed and the generated artifacts. `manifest.loading.*` had zero readers in `packages/core`, `packages/runtime` and `packages/metadata`. So the key parsed, entered the manifest, and changed nothing — #3950, at the scale of a whole block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared process / vm / iframe / web-worker isolation, IPC transports and an `allowedServices` ACL, so an AI author (ADR-0033) reading that vocabulary concluded the platform isolates plugins, wrote the config, and received a clean parse and zero isolation. An inert security control is worse than an absent one because it is believed. Hot reload was additionally a TWO-SOURCE defect: the docs pointed at this dead `PluginHotReloadSchema` while the only implementation body, `HotReloadManager` (`packages/core/src/hot-reload.ts`), reads a different vocabulary — `HotReloadConfigSchema` in `plugin-lifecycle-advanced.zod.ts`. Ruling §2 converges on the surviving side: that schema is KEPT as the starting point for a future enforce decision (it has an implementation body but no runtime composes it yet), and enforcing it is deliberately a separate decision, not this retirement. Why D3 semantic and not a D2 conversion: the chain walks a normalized STACK and `applyConversionsToStoredItem` maps a metadata type onto one of its collections. A package manifest is neither — `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry, so a manifest is not a stack collection member and a stored manifest row passes that seam through unchanged. A conversion would be a transform with no seam that ever runs." diff --git a/packages/spec/src/kernel/manifest.zod.ts b/packages/spec/src/kernel/manifest.zod.ts index ce6a87a5a0..88c8f73d1e 100644 --- a/packages/spec/src/kernel/manifest.zod.ts +++ b/packages/spec/src/kernel/manifest.zod.ts @@ -75,14 +75,39 @@ export const PluginEnginesSchema = z export type PluginEngines = z.input; /** - * Trust / isolation tier the plugin runs under (ADR-0025 §3.6): + * Trust / isolation tier the plugin DECLARES (ADR-0025 §3.6): * - `node` — in-process, full PluginContext (first-party / verified only) * - `sandbox` — QuickJS-WASM, capability-gated surface * - `worker` — out-of-process (reserved) + * + * ⚠️ WHERE THIS IS ENFORCED, AND WHERE IT IS NOT — the two halves are not the + * same answer, so read both before treating a declared tier as isolation. + * + * ENFORCED at the cloud marketplace **publish gate**: an unverified publisher + * requesting the `node` tier is hard-rejected (HTTP 422) and the submission is + * forced to manual review. That gate is a real consumer of this key, which is + * why the tier is not a candidate for retirement. + * + * NOT ENFORCED at load. Load-side enforcement is **not implemented** — nothing + * dispatches on the tier (its only reads are two CLI progress lines that echo + * the value), so a locally installed plugin runs in-process with a full + * PluginContext whatever tier it declares. `sandbox` and `worker` therefore + * name the isolation the publish gate assumes, not isolation the loader + * applies; a local install is not confined by this field. The QuickJS runner + * under `packages/runtime/src/sandbox/` is the hook / action SCRIPT-BODY + * sandbox and is never reached from a plugin's declared tier. + * + * Enforcing the tier at load is tracked as its own decision (v18 direction); + * do not read this field as an isolation guarantee until it lands. */ export const PluginRuntimeSchema = z .enum(['node', 'sandbox', 'worker']) - .describe('Plugin trust tier (ADR-0025 §3.6)'); + .describe( + 'Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud ' + + 'marketplace publish gate (an unverified publisher requesting `node` is rejected with ' + + 'HTTP 422 and forced to manual review); load-side enforcement is NOT implemented, so a ' + + 'locally installed plugin is not isolated by the tier it declares', + ); export type PluginRuntime = z.input; @@ -565,8 +590,12 @@ export const ManifestSchema = z.object({ '(`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.', + 'call. If you were relying on it for isolation, you had none — and the plugin trust ' + + 'tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud ' + + 'marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is ' + + 'rejected with HTTP 422 and forced to manual review), while load-side enforcement is ' + + 'NOT implemented, so a locally installed plugin is not isolated by the tier it ' + + 'declares. Use the permission declarations, which are enforced.', ), /** @@ -596,11 +625,22 @@ export const ManifestSchema = z.object({ .describe('Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`)'), /** - * Trust / isolation tier the plugin runs under (ADR-0025 §3.6). + * Trust / isolation tier the plugin DECLARES (ADR-0025 §3.6). * Unset implies a pure-metadata package (no executable code). + * + * ⚠️ Enforced at the cloud marketplace **publish gate** only — an unverified + * publisher requesting the `node` tier is hard-rejected (HTTP 422) and forced + * to manual review. **Load-side enforcement is not implemented**: a locally + * installed plugin is not isolated by the tier it declares. See + * {@link PluginRuntimeSchema} for the full split. */ runtime: PluginRuntimeSchema.optional() - .describe('Plugin trust tier (ADR-0025 §3.6)'), + .describe( + 'Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud ' + + 'marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual ' + + 'review); load-side enforcement is NOT implemented, so a locally installed plugin is ' + + 'not isolated by the tier it declares', + ), /** * Dependency packaging strategy for code-bearing plugins (ADR-0025 §3.3). diff --git a/packages/spec/src/kernel/plugin-runtime-tier-truthful-text.test.ts b/packages/spec/src/kernel/plugin-runtime-tier-truthful-text.test.ts new file mode 100644 index 0000000000..77f5d1d253 --- /dev/null +++ b/packages/spec/src/kernel/plugin-runtime-tier-truthful-text.test.ts @@ -0,0 +1,149 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { ManifestSchema, PluginRuntimeSchema } from './manifest.zod'; +import { entry as loadingRetiredEntry } from '../migrations/entries/semantic/17.plugin-manifest-loading-retired'; + +// ─── [#11330] `manifest.runtime` text states publish-gate-only enforcement ──── +// +// Maintainer ruling 2026-08-30 (verbatim「同意」), option B: say it truthfully +// NOW; enforcing the tier at load is a v18 direction, deliberately NOT built +// here. Retirement (option C) was ruled OUT — the cloud marketplace publish +// gate is a real consumer of this key, so deleting it would tear out the +// marketplace's trust-gate input. +// +// The defect these pins close is ADR-0049 false compliance with a shipped +// migration message attached. `manifest.loading`'s tombstone redirected every +// upgrading author, verbatim, to "the plugin trust tier (`manifest.runtime`) +// and the permission declarations, which are enforced" — while in this repo +// nothing dispatches on the tier at all (its only reads are two CLI progress +// lines that echo the value). An author who followed the prescription got the +// same nothing the prescription was warning them about, one key over. +// +// The truthful statement is a SPLIT, and both halves are load-bearing: +// • ENFORCED at the cloud marketplace publish gate — an unverified publisher +// requesting the `node` tier is hard-rejected (HTTP 422) and forced to +// manual review. +// • NOT ENFORCED at load — no local isolation follows from the declared tier. +// Pinning only the first half would restore the original lie in softer words; +// pinning only the second would deny a gate that really does run. So every pin +// below asserts BOTH halves, and one asserts the retracted claim is gone — +// without that negative pin, a future edit could re-add "which are enforced" +// beside the new text and every positive assertion would stay green. +// +// ⚠️ On the assertion set: a describe string and a tombstone prescription are +// not a rejection envelope, so there is no ADR-0112 `code` + `status` to pin +// here (the tombstone's own envelope is pinned in +// `plugin-loading-retirement.test.ts`). Where the WORDING is the contract, the +// wording is what gets pinned (#5240). The final case guards the other +// direction — that this text-only change moved no accept/reject behaviour. + +/** Both halves of the truthful split, as separate probes. */ +const SAYS_PUBLISH_GATE_ENFORCED = /publish gate/i; +const SAYS_422 = /422/; +const SAYS_LOAD_SIDE_NOT_ENFORCED = /load-side enforcement is NOT implemented/i; + +describe('[#11330] manifest.runtime trust-tier text is truthful', () => { + const baseManifest = { + id: 'com.example.plugin', + namespace: 'example', + version: '1.0.0', + type: 'app', + name: 'Example Plugin', + } as const; + + it('states BOTH halves on the `PluginRuntime` enum describe', () => { + const text = PluginRuntimeSchema.description ?? ''; + + expect(text, 'the enum must carry a describe at all').not.toBe(''); + expect(text).toMatch(SAYS_PUBLISH_GATE_ENFORCED); + expect(text).toMatch(SAYS_422); + expect(text).toMatch(SAYS_LOAD_SIDE_NOT_ENFORCED); + }); + + it('states BOTH halves on the `manifest.runtime` field describe', () => { + // The field carries its own describe — this is the string that lands in + // `content/docs/references/kernel/manifest.mdx`, i.e. the one an author + // actually reads in the reference table. + const text = ManifestSchema.shape.runtime.description ?? ''; + + expect(text, 'the field must carry its own describe').not.toBe(''); + expect(text).toMatch(SAYS_PUBLISH_GATE_ENFORCED); + expect(text).toMatch(SAYS_422); + expect(text).toMatch(SAYS_LOAD_SIDE_NOT_ENFORCED); + }); + + it('corrects the `loading` tombstone that redirected authors here', () => { + const result = ManifestSchema.safeParse({ + ...baseManifest, + loading: { sandboxing: { enabled: true, isolationLevel: 'process' } }, + }); + + expect(result.success).toBe(false); + if (result.success) return; + + const message = result.error.issues.find((i) => i.path[0] === 'loading')!.message; + + // Still names the tier — the redirection itself is not the defect, the + // unqualified "which are enforced" was. + expect(message).toMatch(/manifest\.runtime/); + expect(message).toMatch(SAYS_PUBLISH_GATE_ENFORCED); + expect(message).toMatch(SAYS_422); + expect(message).toMatch(SAYS_LOAD_SIDE_NOT_ENFORCED); + + // ⛔ The retracted claim must not come back. This is the pin that makes the + // three above mean something: they all stay green if someone re-adds the + // old sentence alongside the new one. + expect( + message, + 'the tombstone must not claim the trust tier is enforced without qualification', + ).not.toMatch(/trust tier \(`manifest\.runtime`\) and the permission declarations, which are enforced/); + }); + + it('leaves the PERMISSIONS half of that sentence verbatim (#11333 owns it)', () => { + // Coordination pin from the ruling: the tombstone sentence has two halves — + // the trust tier (this card) and the permission declarations (#11333). This + // card corrects ONLY its own half. If #11333 later corrects the permissions + // half, this expectation is the thing that goes red and tells that author + // the pin is theirs to update — which is exactly the handoff the ruling + // asked for ("⛔ 不两张各改一半" without a signal between them). + const result = ManifestSchema.safeParse({ ...baseManifest, loading: { strategy: 'lazy' } }); + expect(result.success).toBe(false); + if (result.success) return; + + const message = result.error.issues.find((i) => i.path[0] === 'loading')!.message; + expect(message).toContain('the permission declarations, which are enforced'); + }); + + it('corrects the same claim in the shipped ADR-0087 D3 entry', () => { + // The tombstone ships TWICE — once in the schema, once through this entry + // into `docs/protocol-upgrade-guide.md`. Fixing only the schema would leave + // the upgrade guide still telling authors the tier is a surface "the + // platform actually enforces". + const text = loadingRetiredEntry.replacement; + + expect(text).toMatch(SAYS_PUBLISH_GATE_ENFORCED); + expect(text).toMatch(SAYS_422); + expect(text).toMatch(SAYS_LOAD_SIDE_NOT_ENFORCED); + expect( + text, + 'the D3 entry must not present the trust tier as an enforced surface', + ).not.toMatch(/trust tier\s*\(`manifest\.runtime`, ADR-0025 §3\.6\) and the manifest permission declarations/); + }); + + it('changes NO accept/reject behaviour — every tier still parses as before', () => { + // Anti-vacuity and byte-invariance in one: this card is a text correction, + // so the accepted value set must be untouched. If a future "clarification" + // narrows the enum (e.g. dropping `worker` because it is reserved), that is + // a contract change wearing a text-change costume, and it fails here. + for (const tier of ['node', 'sandbox', 'worker'] as const) { + const result = ManifestSchema.safeParse({ ...baseManifest, runtime: tier }); + expect(result.success, `runtime: '${tier}' must still be accepted`).toBe(true); + } + + expect(ManifestSchema.safeParse({ ...baseManifest, runtime: 'isolated' }).success).toBe(false); + // Unset stays legal — it means a pure-metadata package, which the describe + // still says and which no part of this change touches. + expect(ManifestSchema.safeParse(baseManifest).success).toBe(true); + }); +}); diff --git a/packages/spec/src/migrations/entries/semantic/17.plugin-manifest-loading-retired.ts b/packages/spec/src/migrations/entries/semantic/17.plugin-manifest-loading-retired.ts index c0368f9d66..dcd8c34157 100644 --- a/packages/spec/src/migrations/entries/semantic/17.plugin-manifest-loading-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/17.plugin-manifest-loading-retired.ts @@ -12,8 +12,12 @@ export const entry: SemanticMigration = { + 'registers them and the kernel runs `init` then `start` in an order topologically ' + "resolved from each composed plugin's own `dependencies` / `optionalDependencies` " + '(`resolvePluginOrder` in `packages/core/src/plugin-order.ts`). For the isolation ' - + '`loading.sandboxing` appeared to configure, use the plugin trust tier ' - + '(`manifest.runtime`, ADR-0025 §3.6) and the manifest permission declarations, which ' + + '`loading.sandboxing` appeared to configure, note that the plugin trust tier ' + + '(`manifest.runtime`, ADR-0025 §3.6) does not supply it either: that tier is enforced ' + + 'at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the ' + + '`node` tier is rejected with HTTP 422 and forced to manual review), while load-side ' + + 'enforcement is NOT implemented, so a locally installed plugin is not isolated by the ' + + 'tier it declares. Use the manifest permission declarations, which ' + 'are the surfaces the platform actually enforces', reason: 'ADR-0049 enforce-or-remove; maintainer ruling 2026-08-04 on #4914. The block declared a ' diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 7ab09683f7..a948c5dfc8 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -3872,8 +3872,12 @@ const step17: MigrationStep = { + 'registers them and the kernel runs `init` then `start` in an order topologically ' + "resolved from each composed plugin's own `dependencies` / `optionalDependencies` " + '(`resolvePluginOrder` in `packages/core/src/plugin-order.ts`). For the isolation ' - + '`loading.sandboxing` appeared to configure, use the plugin trust tier ' - + '(`manifest.runtime`, ADR-0025 §3.6) and the manifest permission declarations, which ' + + '`loading.sandboxing` appeared to configure, note that the plugin trust tier ' + + '(`manifest.runtime`, ADR-0025 §3.6) does not supply it either: that tier is enforced ' + + 'at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the ' + + '`node` tier is rejected with HTTP 422 and forced to manual review), while load-side ' + + 'enforcement is NOT implemented, so a locally installed plugin is not isolated by the ' + + 'tier it declares. Use the manifest permission declarations, which ' + 'are the surfaces the platform actually enforces', reason: 'ADR-0049 enforce-or-remove; maintainer ruling 2026-08-04 on #4914. The block declared a '