diff --git a/.changeset/retire-plugin-runtime-family.md b/.changeset/retire-plugin-runtime-family.md
new file mode 100644
index 0000000000..8a44268715
--- /dev/null
+++ b/.changeset/retire-plugin-runtime-family.md
@@ -0,0 +1,116 @@
+---
+"@objectstack/spec": major
+---
+
+refactor(spec)!: remove the `plugin-runtime.zod` family — the "Dynamic Loading" vocabulary no runtime ever implemented (#4834, ADR-0049)
+
+`DynamicLoadRequestSchema`, `DynamicUnloadRequestSchema`,
+`DynamicPluginResultSchema`, `PluginSourceSchema` and
+`DynamicPluginOperationSchema` — with every type alias — are **removed from
+`@objectstack/spec` and `@objectstack/spec/kernel`**. The module they lived in
+is deleted.
+
+Together they declared the platform's "Dynamic Loading" capability: runtime
+load / unload / reload of plugins **without restarting the kernel**, resolved
+from `npm` / `local` / `url` / `registry` / `git` sources, with Subresource
+Integrity hashes, per-plugin sandboxing, `graceful` / `forceful` / `drain`
+unload strategies and `cascade` / `warn` / `block` dependent policy. **None of
+those operations exists.** A bare-name scan of objectstack, cloud and objectui
+(at the commits above) found zero references outside this package's own
+declaration, its unit tests and the generated artifacts: no runtime ever
+received a `DynamicLoadRequest`, performed a load or an unload, or produced a
+`DynamicPluginResult`. Plugins are composed at boot — `defineStack` registers
+them and the kernel runs `init` → `start` — and the set is fixed until the
+process restarts.
+
+This closes a suspension that has been open, and undocumented outside one
+paragraph, since #3896. That change removed this module's discovery/sandbox
+config island and wrote down that "the remainder … also has no runtime consumer
+today; it is left in place because those are operation contracts, not security
+promises, and the enforce-or-remove call on them is a design decision rather
+than a correction." That decision lived only in a changeset, carried by no
+issue. #4834 is the decision, and the answer is **remove**: `experimental` was
+weighed and rejected because it is `.describe()` prose that cannot stop an
+`import` — the weakest of ADR-0049's three channels — and because a
+request/result vocabulary published into the IDE bundle is precisely what an AI
+author (ADR-0033) reads as proof the platform hot-loads plugins, then builds a
+request that parses clean and is received by nobody (#3950).
+
+Migration (FROM → TO):
+
+- `import { DynamicLoadRequestSchema, DynamicUnloadRequestSchema,
+ DynamicPluginResultSchema, PluginSourceSchema, DynamicPluginOperationSchema }
+ from '@objectstack/spec/kernel'` (or from `@objectstack/spec`) →
+ **no replacement export.** Every one is `TS2305: Module … has no exported
+ member` after upgrade, on every public entry. Same for the type aliases
+ `DynamicLoadRequest`, `DynamicUnloadRequest`, `DynamicPluginResult`,
+ `PluginSource`, `DynamicPluginOperation`, `DynamicLoadRequestInput`,
+ `DynamicUnloadRequestInput`.
+- A **`DynamicLoadRequest` / `DynamicUnloadRequest` value** you built → delete
+ it, along with whatever was going to send it. There was never a recipient;
+ the code that constructed one was already a no-op with extra steps. To get a
+ plugin into a running system, put it in the stack (`defineStack`) and restart.
+- A **`DynamicPluginResult`** you typed a handler against → delete the handler.
+ Nothing ever produced one.
+- **`activationEvents` inside a `DynamicLoadRequest`** — the key #4657
+ tombstoned one release-candidate earlier — now has a *stronger* answer than
+ that tombstone gave. #4657 told you: delete this key from your
+ `DynamicLoadRequest`. **The correct instruction is now: delete the entire
+ `DynamicLoadRequest`.** The shape that carried the key is gone, so its
+ `retiredKey()` prescription is gone with it — legitimately, because "this
+ request shape does not exist" is strictly stronger than "this one key of it
+ does not exist". If you are upgrading from v16 and wrote `activationEvents`
+ in *either* form (v16 strings `['onMetadataType:flow']`, or the v17-rc
+ structured `[{ type, pattern }]` from #4653), you do not need to migrate the
+ key at all — the value it sat in has no shape and no recipient.
+ **The studio half of #4657 is unaffected**: `StudioPluginManifest`
+ (`defineStudioPlugin`) is a live authoring surface and still rejects
+ `activationEvents` with its own prescription. Delete the key there.
+- Runtime plugin loading is a **new capability**, not a restoration: if it is
+ ever built it returns via the enforce route of ADR-0049 through a new ADR —
+ loader first, vocabulary second. The shapes it needs are unlikely to be these
+ ones, which is itself a reason not to keep them as a design constraint on
+ work that has not started.
+
+Self-check (#4535 §5):
+
+1. **TS2305 / TS2339 — what exactly breaks?** TS2305 on twelve names, at two
+ entry points (`@objectstack/spec` root and `@objectstack/spec/kernel`) — the
+ five `*Schema` consts and the seven type aliases listed above. No TS2339:
+ nothing removed was a *property* of a surviving shape, because the removed
+ defs were embedded in no parent schema. `PluginSource` was reachable only as
+ `DynamicLoadRequest.source` and `DynamicPluginOperation` only as
+ `DynamicPluginResult.operation`, both of which go in the same change.
+2. **Metadata migration — is there any?** No, and none is possible. All five
+ are root request/result payload shapes: no metadata-type root reaches them
+ (`gen:schema`'s reachability BFS says so — see the gate output below), no
+ `sys_metadata` row can carry one, and no `.stack.ts` / `objectstack.config.ts`
+ authoring surface embeds one. There is therefore no source for an ADR-0087
+ **D2** conversion to rewrite, and `os migrate meta` would have nothing to
+ match. Registered instead as an ADR-0087 **D3** semantic migration,
+ `plugin-runtime-family-retired`, which is where a removal with no rewritable
+ source belongs — the same disposition as #4616, #4767 and #4783. The
+ pre-existing D3 entry `plugin-activation-events-retired` (#4657) is
+ **corrected, not deleted**: its studio half is still live and its historical
+ record must keep replaying, so its kernel half now records this supersession
+ rather than continuing to promise a tombstone that no longer exists.
+3. **Shape change — what kind?** Pure removal of five whole defs; zero
+ additions, zero narrowings, zero renames. Runtime behaviour is unchanged in
+ the strongest sense available: not "equivalent", but *identical*, because no
+ code path anywhere consumed any of it.
+
+The retirement kit: whole-def removal (#4650 route 3 — the defs stop being
+emitted, adjudicated by the `json-schema.manifest.json` ratchet (#2978) and
+`check:api-surface`, not by the per-key tombstone ratchet, which reported all 23
+`authorable-surface.json` deletions as carrying their own proof); no
+`retiredKey()` tombstones, deliberately — nothing parses these schemas any more,
+and a prescription nobody can receive is noise (the precedent this same module
+set in #3896); ADR-0087 D3 semantic migration + the corrected #4657 entry;
+baselines (`json-schema.manifest.json` −5 defs, `authorable-surface.json` −23
+lines, `api-surface.json` −12 names, `spec-changes.json`,
+`docs/protocol-upgrade-guide.md`, `content/docs/references/kernel/`) regenerated
+from the rebuilt source rather than hand-edited; `PLUGIN_STANDARDS.md` §5.3 and
+its capability table now say **Not built** instead of ✅; compiler-API export pin
+(`plugin-runtime-retirement.test.ts` — zero holders for all twelve names across
+every entry in the `package.json` exports map, with three anti-vacuity guards),
+sabotage-verified.
diff --git a/content/docs/references/kernel/index.mdx b/content/docs/references/kernel/index.mdx
index 5eb897a29f..314286cdd8 100644
--- a/content/docs/references/kernel/index.mdx
+++ b/content/docs/references/kernel/index.mdx
@@ -30,7 +30,6 @@ This section contains all protocol schemas for the kernel layer of ObjectStack.
-
diff --git a/content/docs/references/kernel/meta.json b/content/docs/references/kernel/meta.json
index 5bd1cc6acc..5431806b67 100644
--- a/content/docs/references/kernel/meta.json
+++ b/content/docs/references/kernel/meta.json
@@ -6,7 +6,6 @@
"plugin-lifecycle-advanced",
"plugin-loading",
"plugin-registry",
- "plugin-runtime",
"plugin-structure",
"plugin-validator",
"---Plugin Security & Dependencies---",
diff --git a/content/docs/references/kernel/plugin-runtime.mdx b/content/docs/references/kernel/plugin-runtime.mdx
deleted file mode 100644
index 84a258cda3..0000000000
--- a/content/docs/references/kernel/plugin-runtime.mdx
+++ /dev/null
@@ -1,131 +0,0 @@
----
-title: Plugin Runtime
-description: Plugin Runtime protocol schemas
----
-
-{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-
-# Plugin Runtime Management Protocol
-
-Defines the protocol for dynamic plugin loading, unloading, and discovery
-
-at runtime. Addresses the "Dynamic Loading" gap in the microkernel architecture
-
-by enabling plugins to be loaded and unloaded without restarting the kernel.
-
-Inspired by:
-
-- OSGi Dynamic Module System (bundle lifecycle)
-
-- Kubernetes Operator pattern (reconciliation loop)
-
-This protocol enables:
-
-- Runtime load/unload of plugins without kernel restart
-
-- Plugin discovery from registries and local filesystem
-
-- Safe unload with dependency awareness
-
-
-**Source:** `packages/spec/src/kernel/plugin-runtime.zod.ts`
-
-
-## TypeScript Usage
-
-```typescript
-import { DynamicLoadRequestSchema, DynamicPluginOperationSchema, DynamicPluginResultSchema, DynamicUnloadRequestSchema, PluginSourceSchema } from '@objectstack/spec/kernel';
-import type { DynamicLoadRequest, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginSource } from '@objectstack/spec/kernel';
-
-// Validate data
-const result = DynamicLoadRequestSchema.parse(data);
-```
-
----
-
-## DynamicLoadRequest
-
-Request to dynamically load a plugin at runtime
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **pluginId** | `string` | ✅ | Unique plugin identifier |
-| **source** | `{ type: Enum<'npm' \| 'local' \| 'url' \| 'registry' \| 'git'>; location: string; version?: string; integrity?: string }` | ✅ | Plugin source location for dynamic resolution |
-| **activationEvents** | `any` | optional | [REMOVED] `dynamicLoadRequest.activationEvents` was removed in @objectstack/spec 17.0.0 (#4657, ADR-0049) — no runtime ever read it: every plugin activates immediately on load, so the declared lazy-activation window never existed. Delete the key; eager activation is the only behaviour there has ever been. Lazy activation, if built, returns via the enforce route of ADR-0049 with a vocabulary its executor actually honours. |
-| **config** | `Record` | optional | Runtime configuration overrides |
-| **priority** | `integer` | ✅ | Loading priority (lower is higher) |
-| **sandbox** | `boolean` | ✅ | Run in an isolated sandbox |
-| **timeout** | `integer` | ✅ | Maximum time to complete loading in ms |
-
-
----
-
-## DynamicPluginOperation
-
-Runtime plugin operation type
-
-### Allowed Values
-
-* `load`
-* `unload`
-* `reload`
-* `enable`
-* `disable`
-
-
----
-
-## DynamicPluginResult
-
-Result of a dynamic plugin operation
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **success** | `boolean` | ✅ | |
-| **operation** | `Enum<'load' \| 'unload' \| 'reload' \| 'enable' \| 'disable'>` | ✅ | Runtime plugin operation type |
-| **pluginId** | `string` | ✅ | |
-| **durationMs** | `integer` | optional | |
-| **version** | `string` | optional | |
-| **error** | `{ code: string; message: string; details?: Record }` | optional | |
-| **warnings** | `string[]` | optional | |
-
-
----
-
-## DynamicUnloadRequest
-
-Request to dynamically unload a plugin at runtime
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **pluginId** | `string` | ✅ | Plugin to unload |
-| **strategy** | `Enum<'graceful' \| 'forceful' \| 'drain'>` | ✅ | How to handle in-flight work during unload |
-| **timeout** | `integer` | ✅ | Maximum time to complete unloading in ms |
-| **cleanupCache** | `boolean` | ✅ | Remove cached code and assets after unload |
-| **dependentAction** | `Enum<'cascade' \| 'warn' \| 'block'>` | ✅ | How to handle plugins that depend on this one |
-
-
----
-
-## PluginSource
-
-Plugin source location for dynamic resolution
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **type** | `Enum<'npm' \| 'local' \| 'url' \| 'registry' \| 'git'>` | ✅ | Plugin source type |
-| **location** | `string` | ✅ | Package name, file path, URL, or git repository |
-| **version** | `string` | optional | Semver version range (e.g., "^1.0.0") |
-| **integrity** | `string` | optional | Subresource Integrity hash (e.g., "sha384-...") |
-
-
----
-
diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md
index ca65c86f02..2c7ead56ee 100644
--- a/docs/protocol-upgrade-guide.md
+++ b/docs/protocol-upgrade-guide.md
@@ -184,6 +184,8 @@ The same enforce-or-remove pass retires the `RestServerConfig.openApi31` block (
The same pass closes `activationEvents` (#4657): both keys that carried it — `DynamicLoadRequest.activationEvents` on the kernel side and `StudioPluginManifest.activationEvents` on the studio side — declared lazy plugin activation ("plugins remain dormant until an activation event fires") that no runtime in any repo ever implemented: every plugin has always activated immediately on load/registration, and cloud-v1's own ROADMAP recorded the capability as unimplemented, planned for v0.4.0. #4653 had just converged the two `ActivationEventSchema` declarations onto one structured `{ type, pattern }` vocabulary in this same unreleased major; with the maintainer's enforce-or-remove ruling landing on REMOVE, that converged vocabulary retires before ever shipping — composed across the two changes, a v16 author simply deletes the key in whichever form they carried. Neither parent is stored metadata (`StudioPluginManifest` is TS configuration parsed by `defineStudioPlugin`; `DynamicLoadRequest` is a runtime request shape with no caller in any repo), so there is no source for the chain to rewrite — one semantic TODO, the `validateOnly` shape. The kernel key is tombstoned (its schema is not `.strict()`; a plain delete would strip it silently), the studio key is rejected by the strict manifest parse with its own guidance prescription, and the orphaned `ActivationEventSchema` def is removed with them. Behaviour is byte-identical: eager activation was always the only behaviour.
+That kernel-side tombstone was then SUPERSEDED inside the same unreleased major by #4834, which finished the enforce-or-remove pass one level up: the entire `plugin-runtime.zod` family — `DynamicLoadRequest`, `DynamicUnloadRequest`, `DynamicPluginResult`, `PluginSource`, `DynamicPluginOperation` — is removed, because the "Dynamic Loading" capability it described (runtime load / unload / reload without a kernel restart, with sandboxing, integrity hashes, drain strategies and dependent-cascade policy) has no server anywhere: no runtime in objectstack, cloud or objectui ever received one of these requests or produced one of these results. #3896 had suspended the call on these five deliberately — "operation contracts, not security promises" — in a changeset paragraph no issue carried; #4834 is that decision, answered REMOVE. So a v16 author who wrote `activationEvents` inside a `DynamicLoadRequest` value does not delete a key: the whole value has no shape and no recipient, and importing `DynamicLoadRequestSchema` at all is TS2305 in v17. The studio half of the `activationEvents` retirement is untouched and still rejects the key with its own prescription — `defineStudioPlugin` remains a live authoring surface. Behaviour is again byte-identical: nothing ever executed a dynamic plugin operation.
+
Finally it removes the script-body capability token 'crypto.hash' (#4391). Four layers declared it — the `HookBodyCapability` enum, the doc table beside it, the CLI extractor and `ScriptContext.crypto.hash` — and none implemented it: `installCtx` wired only `randomUUID`, so the one call the token authorised threw inside the VM every time. The build-time inference made it worse than an ordinary declared-but-unenforced key: writing `ctx.crypto.hash(...)` made the CLI ADD the capability for you, so `os build` went green on the body that was guaranteed to fail at the first record write. Removed rather than implemented (ADR-0049) — hashing inside the sandbox widens its capability and security-review surface, and a capability that throws on every use yet drew zero complaints in its whole life is its own liveness verdict. This is an enum VALUE, not a key, so there is no `retiredKey()` tombstone: the enum error map carries the prescription, keyed on the received value so that only the spelling which used to be legal is told it "was removed". The conversion strips the dead token from `body.capabilities` on hooks and actions; it deliberately does NOT touch the `ctx.crypto.hash(...)` call the body made under it, which never returned a value and which the author must delete. Hashing returns only WITH an implementation, through the capability admission process.
### Mechanical (applied for you)
@@ -285,8 +287,11 @@ Finally it removes the script-body capability token 'crypto.hash' (#4391). Four
- Why not automatic: The #4484 findStream close-out found `DriverCapabilities.streaming` pointing at a capability the contract no longer declares, and the follow-up audit (#4634) checked every bit in the record the same way, across objectstack and cloud (objectui confirmed clean): of 34 declared bits, THREE have a decision-making reader — `queryDateGranularity` (engine aggregate dispatch + checkDateBucketParity), `autonumber` (engine defers generation to the driver), `batchSchemaSync` (engine ANDs it with method presence, because a subclass can inherit `syncSchemasBatch` from a base whose transport batches while its own cannot) — and THIRTY-ONE were written by every driver and read by nothing. Their `.describe()` strings promised engine adaptation ("if false, ObjectQL will filter/sort/paginate in memory") that was never built, and zero readers let the values go WRONG unnoticed: SqlDriver declared `streaming: false` while implementing `findStream`; InMemoryDriver declared `streaming: true` over a full-table read (ADR-0078 false affordance, on the capability record itself). The real mechanism everywhere else is METHOD presence: transactions gate on `driver.beginTransaction`, aggregate pushdown on `typeof driver.aggregate`, schema sync on `typeof driver.syncSchema`, and the REQUIRED CRUD/bulk methods are called unconditionally. A driver is CODE, never stack metadata — `supports` literals live in driver classes and `DriverConfig.capabilities` is plugin TS configuration, neither ever a `sys_metadata` shape (the stack-tree neighbour, `datasource.capabilities`, was retired separately in #4583) — so there is no source for the D2 chain to rewrite and this entry is the D3 record. The keys are tombstoned rather than deleted because `DriverCapabilitiesSchema` is not `.strict()` and IS parsed (DriverConfigSchema / SQLDriverConfigSchema / NoSQLDriverConfigSchema embed it): a plain delete would silently strip a vendor's authored bit, replacing one silent no-op with another. `batchSchemaSync` also drops its `.default(false)` for `.optional()` — absence already meant false at both readers, and the default forced every capability object to spell out 30+ bits. ADR-0049 / ADR-0078, #4634.
- Done when: No `supports` literal or `DriverConfig.capabilities` object authors any of the 31 retired bits — a driver class that still writes one fails tsc against `IDataDriver.supports` (the bit is `never`), and a parsed config fails with the per-key prescription. The three in-repo drivers (memory / mongodb / sql) declare only live bits; cloud's TursoDriver keeps compiling via its `...super.supports` spread (its stale explicit overrides are cleanup, tracked cloud-side). Engine behaviour is byte-identical: every removed bit had zero readers, and the three live bits keep their readers (engine.ts autonumber defer / aggregate dispatch, plugin.ts + engine.ts batched schema sync, verify date-bucket parity).
- **`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.
- - Done when: No `DynamicLoadRequest` or `defineStudioPlugin` input authors `activationEvents` — authoring it is a tsc error (`never` on the kernel side; an unknown key on the strict studio manifest) and a parse error carrying the prescription on both. 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.
+ - 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` below). 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-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)
+ - Why not automatic: The five schemas declared the "Dynamic Loading" capability — runtime load / unload / reload of plugins without a kernel restart, with sandboxing, integrity hashes, drain strategies and dependent-cascade policy — and NOTHING implemented it. A bare-name scan of objectstack, cloud and objectui found zero references outside this package's own declaration, its unit tests and the generated artifacts: no runtime ever received a `DynamicLoadRequest`, performed a load/unload, or produced a `DynamicPluginResult`. That is the ADR-0049 false-compliance shape at its most inviting to an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` in the published IDE bundle as proof the platform hot-loads plugins and constructs a request that parses clean and is received by nobody (#3950: an exported schema with no consumer is read as a capability). The #3896 follow-up removed this module's discovery/sandbox config island and left these five in place explicitly — "operation contracts, not security promises; the enforce-or-remove call on them is a design decision rather than a correction" — but that suspension lived only in a changeset paragraph with no issue carrying it. #4834 is that decision, answered REMOVE. `experimental` was considered and rejected: it is only `.describe()` prose and cannot stop an import, the weakest of the three ADR-0049 channels. None of the five is stored metadata — they are root request/result payload shapes embedded in no parent schema and parsed against no metadata document — so no `sys_metadata` row can carry one and there is no source for the D2 chain to rewrite; this entry is the D3 record. The removal also subsumes the kernel half of `plugin-activation-events-retired` (#4657): that tombstone goes with the shape that carried it. ADR-0049, #4834.
+ - Done when: No code imports `DynamicLoadRequestSchema`, `DynamicUnloadRequestSchema`, `DynamicPluginResultSchema`, `PluginSourceSchema`, `DynamicPluginOperationSchema` or any of their type aliases (`DynamicLoadRequest`, `DynamicUnloadRequest`, `DynamicPluginResult`, `PluginSource`, `DynamicPluginOperation`, `DynamicLoadRequestInput`, `DynamicUnloadRequestInput`) from `@objectstack/spec` or `@objectstack/spec/kernel` — every one is TS2305 after upgrade, on every public entry (pinned by symbol identity in `plugin-runtime-retirement.test.ts`). Nothing regresses at runtime, because nothing called anything: a caller that believed it was hot-loading a plugin was already only building an object. Boot-time composition through `defineStack` is unchanged.
---
diff --git a/packages/spec/PLUGIN_STANDARDS.md b/packages/spec/PLUGIN_STANDARDS.md
index 5d65f81993..07777b9fd6 100644
--- a/packages/spec/PLUGIN_STANDARDS.md
+++ b/packages/spec/PLUGIN_STANDARDS.md
@@ -156,15 +156,25 @@ Isolation levels: `none`, `process`, `vm`, `iframe`, `web-worker`.
- Transports: `message-port`, `unix-socket`, `tcp`, `memory`
- Configurable message size limit, timeout, and service ACL (`allowedServices`)
-### 5.3 Dynamic Loading (`plugin-runtime.zod.ts`)
-
-Plugins can be loaded and unloaded at runtime **without restarting the kernel**:
-
-- **`DynamicLoadRequestSchema`** — Load a plugin from `npm`, `local`, `url`, `registry`, or `git` sources with optional integrity verification
-- **`DynamicUnloadRequestSchema`** — Graceful/forceful/drain unload with dependency awareness (`cascade`, `warn`, or `block` dependents)
-- ~~`ActivationEventSchema`~~ — REMOVED in v17 (#4657, ADR-0049): the lazy-activation trigger vocabulary had no runtime reader in any repo — every plugin activates immediately on load — so it was retired with the `activationEvents` keys that embedded it. Lazy activation, if built, returns via the enforce route of ADR-0049
-- **`PluginDiscoveryConfigSchema`** — Runtime discovery from registries and local directories with polling and trust filtering
-- **`DynamicLoadingConfigSchema`** — Subsystem configuration: max dynamic plugins, default sandbox policy, allowed sources, integrity requirements
+### 5.3 Dynamic Loading — ~~`plugin-runtime.zod.ts`~~ REMOVED in v17
+
+The whole module is gone (#4834, ADR-0049 enforce-or-remove). It declared runtime
+load / unload / reload of plugins **without restarting the kernel** — sources
+(`npm` / `local` / `url` / `registry` / `git`), integrity hashes, sandboxing,
+graceful/forceful/drain unload, dependent-cascade policy — and **no runtime in
+any repo ever implemented one of those operations**: a bare-name scan of
+objectstack, cloud and objectui found zero references outside the declaration
+itself, its own unit tests and the generated artifacts. Removed with the module:
+`DynamicLoadRequestSchema`, `DynamicUnloadRequestSchema`,
+`DynamicPluginResultSchema`, `PluginSourceSchema`, `DynamicPluginOperationSchema`
+and every type alias. The `activationEvents` tombstone #4657 left on
+`DynamicLoadRequest` goes with the shape that carried it, as did the
+discovery/sandbox config island #3896 had already retired.
+
+**What is real:** plugins are composed at boot. `defineStack` registers them and
+the kernel runs `init` → `start`; the set is fixed until the process restarts.
+Runtime loading, if it is ever built, returns via the enforce route of ADR-0049
+through a new ADR — loader first, vocabulary second.
### 5.4 Plugin System Assessment Summary
@@ -178,4 +188,4 @@ Plugins can be loaded and unloaded at runtime **without restarting the kernel**:
| Health Checks | ✅ | `plugin-lifecycle-advanced.zod.ts` — Per-plugin health + system aggregation |
| Hot Reload | ✅ | `plugin-loading.zod.ts` — Dev + production-safe with rollback and draining |
| Plugin Isolation | ✅ | `plugin-loading.zod.ts` — Configurable scope + IPC for process boundaries |
-| Dynamic Loading | ✅ | `plugin-runtime.zod.ts` — Runtime load/unload (eager activation; the unenforced activation-events vocabulary was removed in #4657) |
+| Dynamic Loading | ❌ | **Not built.** The `plugin-runtime.zod.ts` vocabulary that declared it was removed in v17 (#4834, ADR-0049) — it had no runtime reader in any repo. Plugins are composed at boot; the set is fixed until restart |
diff --git a/packages/spec/PROTOCOL_MAP.md b/packages/spec/PROTOCOL_MAP.md
index 795fabcefc..5c40193877 100644
--- a/packages/spec/PROTOCOL_MAP.md
+++ b/packages/spec/PROTOCOL_MAP.md
@@ -187,7 +187,6 @@ This document serves as the **Grand Map** of the ObjectStack specification. It l
| [`service-registry.zod.ts`](src/kernel/service-registry.zod.ts) | | **Service Registry**. Internal dependency injection. |
| [`metadata-loader.zod.ts`](src/kernel/metadata-loader.zod.ts) | | **Loader**. Logic for loading definitions from disk/DB. |
| [`plugin-loading.zod.ts`](src/kernel/plugin-loading.zod.ts) | ⭐ | **Plugin Loading**. Loading strategies, production-safe hot reload (`environment`, `productionSafety`), full plugin sandboxing (`scope`, `ipc`), code splitting, caching, and performance monitoring. |
-| [`plugin-runtime.zod.ts`](src/kernel/plugin-runtime.zod.ts) | ⭐ | **Dynamic Loading**. Runtime load/unload of plugins (`DynamicLoadRequest`, `DynamicUnloadRequest`), activation events, plugin discovery from registries/directories, and source resolution (npm/local/url/registry/git). |
| [`plugin-registry.zod.ts`](src/kernel/plugin-registry.zod.ts) | | **Plugin Registry**. Metadata for available plugins. |
| [`plugin-security.zod.ts`](src/kernel/plugin-security.zod.ts) | | **Plugin Security**. Security verification for plugins. |
| [`plugin-versioning.zod.ts`](src/kernel/plugin-versioning.zod.ts) | | **Versioning**. Semantic versioning rules for plugins. |
diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json
index fdc04193d7..af816023f9 100644
--- a/packages/spec/api-surface.json
+++ b/packages/spec/api-surface.json
@@ -1488,16 +1488,6 @@
"DisablePackageResponseSchema (const)",
"DistributedStateConfig (type)",
"DistributedStateConfigSchema (const)",
- "DynamicLoadRequest (type)",
- "DynamicLoadRequestInput (type)",
- "DynamicLoadRequestSchema (const)",
- "DynamicPluginOperation (type)",
- "DynamicPluginOperationSchema (const)",
- "DynamicPluginResult (type)",
- "DynamicPluginResultSchema (const)",
- "DynamicUnloadRequest (type)",
- "DynamicUnloadRequestInput (type)",
- "DynamicUnloadRequestSchema (const)",
"EVENT_PRIORITY_VALUES (const)",
"EnablePackageRequest (type)",
"EnablePackageRequestSchema (const)",
@@ -1748,8 +1738,6 @@
"PluginSecurityManifest (type)",
"PluginSecurityManifestSchema (const)",
"PluginSecurityProtocol (const)",
- "PluginSource (type)",
- "PluginSourceSchema (const)",
"PluginStartupResult (type)",
"PluginStartupResultSchema (const)",
"PluginStateSnapshot (type)",
diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json
index ccc3346a9d..e1e005ce6b 100644
--- a/packages/spec/authorable-surface.json
+++ b/packages/spec/authorable-surface.json
@@ -4281,25 +4281,6 @@
"kernel/DistributedStateConfig:provider",
"kernel/DistributedStateConfig:replication",
"kernel/DistributedStateConfig:ttl",
- "kernel/DynamicLoadRequest:activationEvents [RETIRED]",
- "kernel/DynamicLoadRequest:config",
- "kernel/DynamicLoadRequest:pluginId",
- "kernel/DynamicLoadRequest:priority",
- "kernel/DynamicLoadRequest:sandbox",
- "kernel/DynamicLoadRequest:source",
- "kernel/DynamicLoadRequest:timeout",
- "kernel/DynamicPluginResult:durationMs",
- "kernel/DynamicPluginResult:error",
- "kernel/DynamicPluginResult:operation",
- "kernel/DynamicPluginResult:pluginId",
- "kernel/DynamicPluginResult:success",
- "kernel/DynamicPluginResult:version",
- "kernel/DynamicPluginResult:warnings",
- "kernel/DynamicUnloadRequest:cleanupCache",
- "kernel/DynamicUnloadRequest:dependentAction",
- "kernel/DynamicUnloadRequest:pluginId",
- "kernel/DynamicUnloadRequest:strategy",
- "kernel/DynamicUnloadRequest:timeout",
"kernel/EnablePackageRequest:id",
"kernel/EnablePackageResponse:message",
"kernel/EnablePackageResponse:package",
@@ -4909,10 +4890,6 @@
"kernel/PluginSecurityManifest:trustLevel",
"kernel/PluginSecurityManifest:vulnerabilities",
"kernel/PluginSecurityManifest:vulnerabilityDisclosure",
- "kernel/PluginSource:integrity",
- "kernel/PluginSource:location",
- "kernel/PluginSource:type",
- "kernel/PluginSource:version",
"kernel/PluginStartupResult:duration",
"kernel/PluginStartupResult:error",
"kernel/PluginStartupResult:health",
diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json
index 798a505df7..51571b27f9 100644
--- a/packages/spec/json-schema.manifest.json
+++ b/packages/spec/json-schema.manifest.json
@@ -907,10 +907,6 @@
"kernel/DisablePackageRequest",
"kernel/DisablePackageResponse",
"kernel/DistributedStateConfig",
- "kernel/DynamicLoadRequest",
- "kernel/DynamicPluginOperation",
- "kernel/DynamicPluginResult",
- "kernel/DynamicUnloadRequest",
"kernel/EnablePackageRequest",
"kernel/EnablePackageResponse",
"kernel/Event",
@@ -1021,7 +1017,6 @@
"kernel/PluginSandboxing",
"kernel/PluginSearchFilters",
"kernel/PluginSecurityManifest",
- "kernel/PluginSource",
"kernel/PluginStartupResult",
"kernel/PluginStateSnapshot",
"kernel/PluginStatistics",
diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json
index 8ed80cbecf..eed53d0e6a 100644
--- a/packages/spec/spec-changes.json
+++ b/packages/spec/spec-changes.json
@@ -493,7 +493,14 @@
"replacement": "(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)",
"migrationId": "plugin-activation-events-retired",
"toMajor": 17,
- "rationale": "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."
+ "rationale": "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` below). The studio half of this entry is unaffected and still enforced by the strict manifest parse."
+ },
+ {
+ "surface": "kernel.dynamicLoadRequest / kernel.dynamicUnloadRequest / kernel.dynamicPluginResult / kernel.pluginSource / kernel.dynamicPluginOperation",
+ "replacement": "(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)",
+ "migrationId": "plugin-runtime-family-retired",
+ "toMajor": 17,
+ "rationale": "The five schemas declared the \"Dynamic Loading\" capability — runtime load / unload / reload of plugins without a kernel restart, with sandboxing, integrity hashes, drain strategies and dependent-cascade policy — and NOTHING implemented it. A bare-name scan of objectstack, cloud and objectui found zero references outside this package's own declaration, its unit tests and the generated artifacts: no runtime ever received a `DynamicLoadRequest`, performed a load/unload, or produced a `DynamicPluginResult`. That is the ADR-0049 false-compliance shape at its most inviting to an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` in the published IDE bundle as proof the platform hot-loads plugins and constructs a request that parses clean and is received by nobody (#3950: an exported schema with no consumer is read as a capability). The #3896 follow-up removed this module's discovery/sandbox config island and left these five in place explicitly — \"operation contracts, not security promises; the enforce-or-remove call on them is a design decision rather than a correction\" — but that suspension lived only in a changeset paragraph with no issue carrying it. #4834 is that decision, answered REMOVE. `experimental` was considered and rejected: it is only `.describe()` prose and cannot stop an import, the weakest of the three ADR-0049 channels. None of the five is stored metadata — they are root request/result payload shapes embedded in no parent schema and parsed against no metadata document — so no `sys_metadata` row can carry one and there is no source for the D2 chain to rewrite; this entry is the D3 record. The removal also subsumes the kernel half of `plugin-activation-events-retired` (#4657): that tombstone goes with the shape that carried it. ADR-0049, #4834."
}
],
"removed": []
@@ -1046,7 +1053,14 @@
"replacement": "(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)",
"migrationId": "plugin-activation-events-retired",
"toMajor": 17,
- "rationale": "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."
+ "rationale": "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` below). The studio half of this entry is unaffected and still enforced by the strict manifest parse."
+ },
+ {
+ "surface": "kernel.dynamicLoadRequest / kernel.dynamicUnloadRequest / kernel.dynamicPluginResult / kernel.pluginSource / kernel.dynamicPluginOperation",
+ "replacement": "(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)",
+ "migrationId": "plugin-runtime-family-retired",
+ "toMajor": 17,
+ "rationale": "The five schemas declared the \"Dynamic Loading\" capability — runtime load / unload / reload of plugins without a kernel restart, with sandboxing, integrity hashes, drain strategies and dependent-cascade policy — and NOTHING implemented it. A bare-name scan of objectstack, cloud and objectui found zero references outside this package's own declaration, its unit tests and the generated artifacts: no runtime ever received a `DynamicLoadRequest`, performed a load/unload, or produced a `DynamicPluginResult`. That is the ADR-0049 false-compliance shape at its most inviting to an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` in the published IDE bundle as proof the platform hot-loads plugins and constructs a request that parses clean and is received by nobody (#3950: an exported schema with no consumer is read as a capability). The #3896 follow-up removed this module's discovery/sandbox config island and left these five in place explicitly — \"operation contracts, not security promises; the enforce-or-remove call on them is a design decision rather than a correction\" — but that suspension lived only in a changeset paragraph with no issue carrying it. #4834 is that decision, answered REMOVE. `experimental` was considered and rejected: it is only `.describe()` prose and cannot stop an import, the weakest of the three ADR-0049 channels. None of the five is stored metadata — they are root request/result payload shapes embedded in no parent schema and parsed against no metadata document — so no `sys_metadata` row can carry one and there is no source for the D2 chain to rewrite; this entry is the D3 record. The removal also subsumes the kernel half of `plugin-activation-events-retired` (#4657): that tombstone goes with the shape that carried it. ADR-0049, #4834."
}
],
"removed": []
diff --git a/packages/spec/src/kernel/activation-events-retirement.test.ts b/packages/spec/src/kernel/activation-events-retirement.test.ts
index 82cc32aba6..a928f6548b 100644
--- a/packages/spec/src/kernel/activation-events-retirement.test.ts
+++ b/packages/spec/src/kernel/activation-events-retirement.test.ts
@@ -23,9 +23,13 @@ import { describe, it, expect } from 'vitest';
// load-bearing pin is the program below, with anti-vacuity guards — the
// #4737 `ActionLocation` retirement's machinery, pointed at absence instead of
// ownership. Sabotage-verified in the PR: resurrecting the declaration in
-// `plugin-runtime.zod.ts` turns it red, re-exporting ANY schema under the bare
-// name from `./studio` turns it red, and pointing the enumeration at nothing
-// trips the anti-vacuity guards rather than passing silently.
+// `./kernel` turns it red, re-exporting ANY schema under the bare name from
+// `./studio` turns it red, and pointing the enumeration at nothing trips the
+// anti-vacuity guards rather than passing silently.
+//
+// (#4834 superseded the kernel half: `DynamicLoadRequest` — the shape that
+// carried the tombstoned key — was removed whole, so this file's kernel-side
+// parse assertion is gone. See the block above the studio test.)
describe('[#4657] ActivationEventSchema removal — no entry exports the name', () => {
it('resolves the export surface: the retired names have ZERO holders across every public entry', async () => {
const ts = (await import('typescript')).default;
@@ -87,8 +91,12 @@ describe('[#4657] ActivationEventSchema removal — no entry exports the name',
const studioNames = exportsOf('./studio').map((e) => e.getName());
expect(kernelNames.length, './kernel must export a non-trivial surface').toBeGreaterThan(40);
expect(studioNames.length, './studio must export a non-trivial surface').toBeGreaterThan(40);
- expect(kernelNames).toContain('DynamicLoadRequestSchema');
- expect(kernelNames).toContain('PluginSourceSchema');
+ // (The kernel anchors used to be `DynamicLoadRequestSchema` /
+ // `PluginSourceSchema` — the parents of the retired key. #4834 removed that
+ // whole family, so the anchors move to surviving kernel neighbours; the
+ // assertion they serve is unchanged.)
+ expect(kernelNames).toContain('PluginSchema');
+ expect(kernelNames).toContain('PluginContextSchema');
expect(studioNames).toContain('StudioPluginManifestSchema');
expect(studioNames).toContain('StudioPluginContributionsSchema');
@@ -110,29 +118,28 @@ describe('[#4657] ActivationEventSchema removal — no entry exports the name',
expect('ActivationEventSchema' in ns, `${label} must not export ActivationEventSchema`).toBe(false);
}
// Anti-vacuity: the namespaces just probed are real and non-trivial.
- expect('DynamicLoadRequestSchema' in kernel).toBe(true);
+ expect('PluginSchema' in kernel).toBe(true);
expect('StudioPluginManifestSchema' in studio).toBe(true);
});
- it('the live paths still parse — and reject the retired key with its prescription', async () => {
- const { DynamicLoadRequestSchema } = await import('./plugin-runtime.zod');
+ // ─── The kernel half of #4657 is SUPERSEDED by #4834 ───────────────────────
+ //
+ // `DynamicLoadRequest.activationEvents` was tombstoned here with a
+ // `retiredKey()` prescription. #4834 removed the ENTIRE `DynamicLoadRequest`
+ // family (ADR-0049 enforce-or-remove on the whole "Dynamic Loading"
+ // capability, four-repo zero-consumer), which took the tombstone with it —
+ // legitimately: "the whole request shape is gone" is strictly stronger than
+ // "this one key is gone", and there is no longer a parse for a prescription
+ // to be delivered at. What replaces it as the kernel-side pin is
+ // `plugin-runtime-retirement.test.ts` (zero holders for all five names),
+ // plus the zero-holder assertion above, which covers `ActivationEventSchema`
+ // itself on both entries. Only the studio half still has a live parse:
+ it('the studio manifest still parses — and rejects the retired key with its prescription', async () => {
const { StudioPluginManifestSchema } = await import('../studio/plugin.zod');
- // Kernel side: tombstoned (non-strict schema — a plain delete would have
- // Zod silently strip an authored value, replacing one silent no-op with
- // another).
- const load = { pluginId: 'com.acme.analytics', source: { type: 'npm', location: '@acme/x' } };
- expect(DynamicLoadRequestSchema.parse(load)).not.toHaveProperty('activationEvents');
- expect(() =>
- DynamicLoadRequestSchema.parse({
- ...load,
- activationEvents: [{ type: 'onStartup', pattern: '*' }],
- }),
- ).toThrow(/activationEvents.*removed.*#4657/s);
-
- // Studio side: strict parse + guidance — the SAME document differing only
- // in this one key stays legal without it (so the negative cannot pass for
- // an unrelated reason).
+ // Strict parse + guidance — the SAME document differing only in this one
+ // key stays legal without it (so the negative cannot pass for an unrelated
+ // reason).
const manifest = { id: 'objectstack.my-plugin', name: 'My Plugin' };
expect(StudioPluginManifestSchema.parse(manifest)).not.toHaveProperty('activationEvents');
expect(() =>
diff --git a/packages/spec/src/kernel/index.ts b/packages/spec/src/kernel/index.ts
index c58c77a90d..a3dd53187e 100644
--- a/packages/spec/src/kernel/index.ts
+++ b/packages/spec/src/kernel/index.ts
@@ -41,7 +41,39 @@ export * from './package-upgrade.zod';
export * from './plugin-capability.zod';
export * from './plugin-lifecycle-advanced.zod';
export * from './plugin-loading.zod';
-export * from './plugin-runtime.zod';
+// plugin-runtime.zod (DynamicLoadRequestSchema / DynamicUnloadRequestSchema /
+// DynamicPluginResultSchema / PluginSourceSchema / DynamicPluginOperationSchema
+// + every type alias) was REMOVED per ADR-0049 enforce-or-remove (#4834). The
+// module declared the "Dynamic Loading" capability — runtime load / unload /
+// reload of plugins without a kernel restart, with sandboxing, integrity hashes,
+// drain strategies and dependent-cascade policy. No runtime in any repo
+// (objectstack / cloud / objectui) ever received a DynamicLoadRequest, performed
+// a load/unload, or produced a DynamicPluginResult: the operations the vocabulary
+// names do not exist. Plugins are composed at boot (`defineStack` → the kernel's
+// register/init/start sequence) and the set is fixed until the process restarts.
+//
+// The #3896 follow-up removed this module's discovery/sandbox config island and
+// recorded the remaining five as a deliberate SUSPENSION ("operation contracts,
+// not security promises; the enforce-or-remove call is a design decision") — a
+// decision that then lived only in a changeset paragraph, with no issue carrying
+// it. #4834 IS that decision, resolved REMOVE: a published request/result
+// vocabulary with no server behind it is the #3950 shape at its most inviting to
+// an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` as proof the
+// platform hot-loads plugins and builds a request that parses clean and is
+// received by nobody.
+//
+// This SUPERSEDES the narrower #4657 retirement of
+// `DynamicLoadRequest.activationEvents`: that key's `retiredKey()` tombstone is
+// removed together with the shape that carried it, because "the whole request
+// shape is gone" is strictly stronger than "this one key is gone" — an author
+// who wrote `activationEvents` now deletes the entire `DynamicLoadRequest`
+// value, not one key of it. The studio half of #4657
+// (`StudioPluginManifest.activationEvents`) is untouched and still rejects the
+// key with its own prescription.
+//
+// Runtime plugin loading, if it is ever built, returns via the enforce route of
+// ADR-0049 through a new ADR — write the loader first, then declare exactly the
+// operations it performs. The vocabulary it needs is unlikely to be this one.
export * from './plugin-security-advanced.zod';
export * from './plugin-structure.zod';
export * from './plugin-validator.zod';
diff --git a/packages/spec/src/kernel/plugin-runtime-retirement.test.ts b/packages/spec/src/kernel/plugin-runtime-retirement.test.ts
new file mode 100644
index 0000000000..b1cac906dd
--- /dev/null
+++ b/packages/spec/src/kernel/plugin-runtime-retirement.test.ts
@@ -0,0 +1,140 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+import { describe, it, expect } from 'vitest';
+
+// ─── [#4834] the `plugin-runtime.zod` family is REMOVED ─────────────────────
+//
+// ADR-0049 enforce-or-remove, ruled REMOVE. `DynamicLoadRequestSchema`,
+// `DynamicUnloadRequestSchema`, `DynamicPluginResultSchema`,
+// `PluginSourceSchema` and `DynamicPluginOperationSchema` (+ every type alias)
+// declared the "Dynamic Loading" capability — runtime load / unload / reload of
+// plugins without a kernel restart. No runtime in objectstack / cloud /
+// objectui ever received one of these requests or produced one of these
+// results: the operations the vocabulary names do not exist. #3896 recorded the
+// suspension as a deliberate design decision that lived only in a changeset
+// paragraph; #4834 is that decision, answered.
+//
+// Why THIS pin, and not a type-level one: #4642 established that a
+// compile-time conditional-type assertion in this package is a no-op (the
+// package tsconfig excludes `**/*.test.ts`, and vitest never enables
+// `typecheck`), so a `Assert< Equal< … > >` here would be decoration. The
+// load-bearing pin is the TypeScript compiler-API program below, which resolves
+// the REAL export surface of EVERY public entry from `package.json`'s exports
+// map and asserts each retired name has zero holders — by symbol identity, not
+// by grepping text.
+//
+// Every `not`-shaped assertion has an anti-vacuity guard, because the failure
+// mode of an absence pin is passing for the wrong reason (a path typo, an entry
+// that stops resolving, an empty enumeration). Sabotage-verified in the PR:
+// 1. re-declare `PluginSourceSchema` in `./kernel` → red;
+// 2. re-export any of the five names from a DIFFERENT entry (`./studio`) →
+// red (a re-export can lie about the domain even when the symbol is
+// honest — the C14/C15 lesson);
+// 3. point the entry enumeration at nothing → the anti-vacuity guards trip
+// instead of the suite passing silently.
+describe('[#4834] plugin-runtime family removal — no entry exports any of the five names', () => {
+ /** The five schema names + every type alias they published. */
+ const RETIRED = [
+ 'DynamicLoadRequestSchema',
+ 'DynamicUnloadRequestSchema',
+ 'DynamicPluginResultSchema',
+ 'PluginSourceSchema',
+ 'DynamicPluginOperationSchema',
+ 'DynamicLoadRequest',
+ 'DynamicUnloadRequest',
+ 'DynamicPluginResult',
+ 'PluginSource',
+ 'DynamicPluginOperation',
+ 'DynamicLoadRequestInput',
+ 'DynamicUnloadRequestInput',
+ ] as const;
+
+ it('resolves the export surface: the retired names have ZERO holders across every public entry', async () => {
+ const ts = (await import('typescript')).default;
+ const { resolve, dirname } = await import('node:path');
+ const { fileURLToPath } = await import('node:url');
+ const { readFileSync } = await import('node:fs');
+
+ const specDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
+ // Every public entry point, read from package.json's exports map so a
+ // future entry cannot silently escape the absence assertions below.
+ const pkg = JSON.parse(readFileSync(resolve(specDir, 'package.json'), 'utf8')) as {
+ exports: Record;
+ };
+ const entries: Record = {};
+ for (const sub of Object.keys(pkg.exports)) {
+ if (sub === '.') entries[sub] = resolve(specDir, 'src/index.ts');
+ else if (/^\.\/[a-z-]+$/.test(sub)) entries[sub] = resolve(specDir, `src/${sub.slice(2)}/index.ts`);
+ // './openapi.json' / './package.json' are not TypeScript entry points.
+ }
+ // Anti-vacuity (1): the enumeration found the real surface — including the
+ // entry that used to own every retired name, and the root barrel that
+ // re-exported it.
+ for (const needed of ['.', './kernel']) {
+ expect(Object.keys(entries), `exports map must include ${needed}`).toContain(needed);
+ }
+ expect(Object.keys(entries).length).toBeGreaterThan(10);
+
+ const program = ts.createProgram(Object.values(entries), {
+ module: ts.ModuleKind.ESNext,
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
+ skipLibCheck: true,
+ noEmit: true,
+ });
+ const checker = program.getTypeChecker();
+
+ const exportsOf = (sub: string) => {
+ const sf = program.getSourceFile(entries[sub]);
+ const moduleSym = sf && checker.getSymbolAtLocation(sf);
+ // Anti-vacuity (2): without this guard a resolution failure would make
+ // every absence assertion below pass for free — exactly how a gate goes
+ // dormant (#4642).
+ expect(moduleSym, `${sub} module symbol must resolve`).toBeTruthy();
+ return checker.getExportsOfModule(moduleSym!);
+ };
+
+ /** Every entry that exports `name` — for a removal this must be []. */
+ const holdersOf = (name: string): string[] => {
+ const out: string[] = [];
+ for (const sub of Object.keys(entries)) {
+ if (exportsOf(sub).some((e) => e.getName() === name)) out.push(sub);
+ }
+ return out;
+ };
+
+ // Anti-vacuity (3): `holdersOf` actually finds holders when a name IS
+ // exported — proven on a surviving kernel neighbour, so `[]` below means
+ // "absent", not "the probe is broken".
+ expect(holdersOf('PluginSchema')).toContain('./kernel');
+ const kernelNames = exportsOf('./kernel').map((e) => e.getName());
+ expect(kernelNames.length, './kernel must still export a non-trivial surface').toBeGreaterThan(40);
+
+ // The removal itself: NO public entry exports any of the twelve names —
+ // not the old owner, and not some other entry that might "helpfully" adopt
+ // them. Exact equality with `[]`, so a partial move cannot slip through.
+ for (const name of RETIRED) {
+ expect(holdersOf(name), `${name} must have zero holders`).toEqual([]);
+ }
+ });
+
+ it('keeps the runtime namespaces consistent with the compiler view', async () => {
+ const kernel = await import('./index');
+ const root = await import('../index');
+
+ for (const [label, ns] of [['./kernel', kernel], ['.', root]] as const) {
+ for (const name of RETIRED) {
+ expect(name in ns, `${label} must not export ${name}`).toBe(false);
+ }
+ }
+ // Anti-vacuity: the namespaces just probed are real and non-trivial.
+ expect('PluginSchema' in kernel).toBe(true);
+ expect(Object.keys(kernel).length).toBeGreaterThan(40);
+ });
+
+ it('the module itself is gone — nothing can import it by path either', async () => {
+ // The names could be absent from the barrels while the module still sat on
+ // disk, importable by deep path and still emitting json-schema defs. It
+ // does not.
+ await expect(import('./plugin-runtime.zod')).rejects.toThrow();
+ });
+});
diff --git a/packages/spec/src/kernel/plugin-runtime.test.ts b/packages/spec/src/kernel/plugin-runtime.test.ts
deleted file mode 100644
index 4d80edd74e..0000000000
--- a/packages/spec/src/kernel/plugin-runtime.test.ts
+++ /dev/null
@@ -1,200 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import {
- DynamicPluginOperationSchema,
- PluginSourceSchema,
- DynamicLoadRequestSchema,
- DynamicUnloadRequestSchema,
- DynamicPluginResultSchema,
-} from './plugin-runtime.zod';
-
-describe('Plugin Runtime Management Protocol', () => {
- describe('DynamicPluginOperationSchema', () => {
- it('should accept valid operations', () => {
- expect(DynamicPluginOperationSchema.parse('load')).toBe('load');
- expect(DynamicPluginOperationSchema.parse('unload')).toBe('unload');
- expect(DynamicPluginOperationSchema.parse('reload')).toBe('reload');
- expect(DynamicPluginOperationSchema.parse('enable')).toBe('enable');
- expect(DynamicPluginOperationSchema.parse('disable')).toBe('disable');
- });
-
- it('should reject invalid operations', () => {
- expect(() => DynamicPluginOperationSchema.parse('invalid')).toThrow();
- });
- });
-
- describe('PluginSourceSchema', () => {
- it('should accept npm source', () => {
- const source = {
- type: 'npm',
- location: '@objectstack/plugin-analytics',
- version: '^2.0.0',
- };
- const result = PluginSourceSchema.parse(source);
- expect(result.type).toBe('npm');
- expect(result.version).toBe('^2.0.0');
- });
-
- it('should accept local source', () => {
- const source = {
- type: 'local',
- location: '/opt/plugins/custom-plugin',
- };
- const result = PluginSourceSchema.parse(source);
- expect(result.type).toBe('local');
- expect(result.version).toBeUndefined();
- });
-
- it('should accept url source with integrity', () => {
- const source = {
- type: 'url',
- location: 'https://plugins.example.com/analytics-1.0.0.tgz',
- integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC',
- };
- const result = PluginSourceSchema.parse(source);
- expect(result.integrity).toBeDefined();
- });
-
- it('should accept all source types', () => {
- const types = ['npm', 'local', 'url', 'registry', 'git'];
- types.forEach((type) => {
- const result = PluginSourceSchema.parse({ type, location: 'test' });
- expect(result.type).toBe(type);
- });
- });
- });
-
- // `ActivationEventSchema` was REMOVED (#4657, ADR-0049) — no runtime ever
- // read an activation event, so the vocabulary retired with the keys that
- // embedded it. Export-surface pins live in
- // activation-events-retirement.test.ts; the tombstone pins are below.
-
- describe('DynamicLoadRequestSchema', () => {
- it('should accept minimal load request', () => {
- const request = {
- pluginId: 'com.acme.analytics',
- source: {
- type: 'npm' as const,
- location: '@acme/analytics-plugin',
- version: '^1.0.0',
- },
- };
- const result = DynamicLoadRequestSchema.parse(request);
- expect(result.pluginId).toBe('com.acme.analytics');
- expect(result.priority).toBe(100); // default
- expect(result.sandbox).toBe(false); // default
- expect(result.timeout).toBe(60000); // default
- });
-
- it('should accept full load request', () => {
- const request = {
- pluginId: 'com.acme.analytics',
- source: {
- type: 'registry' as const,
- location: 'acme-analytics',
- version: '~2.1.0',
- },
- config: { apiKey: 'abc123', region: 'us-east' },
- priority: 50,
- sandbox: true,
- timeout: 120000,
- };
- const result = DynamicLoadRequestSchema.parse(request);
- expect(result.sandbox).toBe(true);
- expect(result.priority).toBe(50);
- });
-
- // ─── [#4657] `activationEvents` tombstone pins (ADR-0049) ────────────
- // The key promised lazy activation no runtime ever implemented — every
- // plugin activates immediately on load. The schema is not `.strict()`,
- // so the removal is a `retiredKey()` tombstone: a plain delete would have
- // Zod silently STRIP an authored value (#2169 shape) instead of teaching.
- it('rejects an authored activationEvents with the retirement prescription', () => {
- expect(() =>
- DynamicLoadRequestSchema.parse({
- pluginId: 'com.acme.analytics',
- source: { type: 'npm' as const, location: '@acme/analytics-plugin' },
- activationEvents: [{ type: 'onRoute', pattern: '/api/v1/analytics/*' }],
- }),
- ).toThrow(/activationEvents.*removed.*17\.0\.0.*#4657.*Delete the key/s);
- });
-
- it('parses clean without the key — and the result does not carry it', () => {
- const result = DynamicLoadRequestSchema.parse({
- pluginId: 'com.acme.analytics',
- source: { type: 'npm' as const, location: '@acme/analytics-plugin' },
- });
- // Absence stays absence: the tombstone must not materialize a value.
- expect(result).not.toHaveProperty('activationEvents');
- });
- });
-
- describe('DynamicUnloadRequestSchema', () => {
- it('should accept minimal unload request', () => {
- const request = {
- pluginId: 'com.acme.analytics',
- };
- const result = DynamicUnloadRequestSchema.parse(request);
- expect(result.strategy).toBe('graceful'); // default
- expect(result.timeout).toBe(30000); // default
- expect(result.cleanupCache).toBe(false); // default
- expect(result.dependentAction).toBe('block'); // default
- });
-
- it('should accept full unload request', () => {
- const request = {
- pluginId: 'com.acme.analytics',
- strategy: 'drain' as const,
- timeout: 60000,
- cleanupCache: true,
- dependentAction: 'cascade' as const,
- };
- const result = DynamicUnloadRequestSchema.parse(request);
- expect(result.strategy).toBe('drain');
- expect(result.cleanupCache).toBe(true);
- expect(result.dependentAction).toBe('cascade');
- });
-
- it('should accept all unload strategies', () => {
- const strategies = ['graceful', 'forceful', 'drain'];
- strategies.forEach((strategy) => {
- const result = DynamicUnloadRequestSchema.parse({
- pluginId: 'test',
- strategy,
- });
- expect(result.strategy).toBe(strategy);
- });
- });
- });
-
- describe('DynamicPluginResultSchema', () => {
- it('should accept successful result', () => {
- const result = DynamicPluginResultSchema.parse({
- success: true,
- operation: 'load',
- pluginId: 'com.acme.analytics',
- durationMs: 1500,
- version: '1.2.3',
- });
- expect(result.success).toBe(true);
- expect(result.version).toBe('1.2.3');
- });
-
- it('should accept failed result with error', () => {
- const result = DynamicPluginResultSchema.parse({
- success: false,
- operation: 'load',
- pluginId: 'com.acme.analytics',
- error: {
- code: 'DEPENDENCY_MISSING',
- message: 'Required dependency "postgres" is not loaded',
- details: { missing: ['postgres'] },
- },
- warnings: ['Plugin was partially loaded before failure'],
- });
- expect(result.success).toBe(false);
- expect(result.error?.code).toBe('DEPENDENCY_MISSING');
- expect(result.warnings).toHaveLength(1);
- });
- });
-
-});
diff --git a/packages/spec/src/kernel/plugin-runtime.zod.ts b/packages/spec/src/kernel/plugin-runtime.zod.ts
deleted file mode 100644
index 678bf6d7c7..0000000000
--- a/packages/spec/src/kernel/plugin-runtime.zod.ts
+++ /dev/null
@@ -1,275 +0,0 @@
-// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
-
-import { z } from 'zod';
-
-/**
- * # Plugin Runtime Management Protocol
- *
- * Defines the protocol for dynamic plugin loading, unloading, and discovery
- * at runtime. Addresses the "Dynamic Loading" gap in the microkernel architecture
- * by enabling plugins to be loaded and unloaded without restarting the kernel.
- *
- * Inspired by:
- * - OSGi Dynamic Module System (bundle lifecycle)
- * - Kubernetes Operator pattern (reconciliation loop)
- *
- * This protocol enables:
- * - Runtime load/unload of plugins without kernel restart
- * - Plugin discovery from registries and local filesystem
- * - Safe unload with dependency awareness
- */
-
-/**
- * Dynamic Plugin Operation Type
- * Operations that can be performed on plugins at runtime
- */
-import { lazySchema } from '../shared/lazy-schema';
-import { retiredKey } from '../shared/retired-key';
-export const DynamicPluginOperationSchema = lazySchema(() => z.enum([
- 'load', // Load and initialize a plugin at runtime
- 'unload', // Gracefully unload a running plugin
- 'reload', // Unload then load (e.g., version upgrade)
- 'enable', // Enable a loaded but disabled plugin
- 'disable', // Disable a running plugin without unloading
-]).describe('Runtime plugin operation type'));
-
-/**
- * Plugin Source
- * Where to resolve a plugin for dynamic loading
- */
-export const PluginSourceSchema = lazySchema(() => z.object({
- /**
- * Source type
- */
- type: z.enum([
- 'npm', // npm registry package
- 'local', // Local filesystem path
- 'url', // Remote URL (tarball or module)
- 'registry', // ObjectStack plugin registry
- 'git', // Git repository
- ]).describe('Plugin source type'),
-
- /**
- * Source location (package name, path, URL, or git repo)
- */
- location: z.string().describe('Package name, file path, URL, or git repository'),
-
- /**
- * Version constraint (semver range)
- */
- version: z.string().optional().describe('Semver version range (e.g., "^1.0.0")'),
-
- /**
- * Integrity hash for verification
- */
- integrity: z.string().optional().describe('Subresource Integrity hash (e.g., "sha384-...")'),
-}).describe('Plugin source location for dynamic resolution'));
-
-/**
- * REMOVED — `ActivationEventSchema` / `ActivationEvent` (#4657, ADR-0049).
- *
- * The schema declared a lazy-activation trigger vocabulary (`onCommand`,
- * `onRoute`, …, `onView` after the #4653 convergence) that NO runtime in any
- * repo (objectstack / cloud / cloud-v1 / objectui) ever read: every plugin has
- * always activated immediately on load/registration — cloud-v1's own ROADMAP
- * recorded lazy activation as ❌ unimplemented. A published trigger vocabulary
- * with zero executors is the ADR-0049 false-compliance shape: an author (very
- * often an AI, ADR-0033) writes `activationEvents` expecting deferral and gets
- * eager activation with a clean parse.
- *
- * Both keys that embedded it are retired in the same change —
- * `DynamicLoadRequestSchema.activationEvents` (tombstoned below) and
- * `StudioPluginManifestSchema.activationEvents` (rejected by that schema's
- * strict parse with its own prescription) — which left the def an orphaned
- * value schema: an export with no consumer is read as a capability by whoever
- * finds it (#3950), so it goes with the keys rather than outliving them. Its
- * `json-schema.manifest.json` entries (`kernel/ActivationEvent`,
- * `studio/ActivationEvent`) and `authorable-surface.json` lines are dropped
- * deliberately in the same PR; the removal is pinned by
- * `activation-events-retirement.test.ts`.
- *
- * Lazy activation, if ever built, returns via the enforce route of ADR-0049:
- * write the executor first, then declare exactly the vocabulary it honours.
- */
-
-const ACTIVATION_EVENTS_RETIRED =
- '`dynamicLoadRequest.activationEvents` was removed in @objectstack/spec 17.0.0 '
- + '(#4657, ADR-0049) — no runtime ever read it: every plugin activates immediately '
- + 'on load, so the declared lazy-activation window never existed. Delete the key; '
- + 'eager activation is the only behaviour there has ever been. Lazy activation, if '
- + 'built, returns via the enforce route of ADR-0049 with a vocabulary its executor '
- + 'actually honours.';
-
-/**
- * Dynamic Load Request
- * Request to load a plugin at runtime
- */
-export const DynamicLoadRequestSchema = lazySchema(() => z.object({
- /**
- * Plugin identifier to load
- */
- pluginId: z.string().describe('Unique plugin identifier'),
-
- /**
- * Plugin source
- */
- source: PluginSourceSchema,
-
- /**
- * RETIRED (#4657, ADR-0049) — tombstoned, not deleted: this schema is not
- * `.strict()`, so a plain delete would have Zod silently STRIP the key and
- * replace one silent no-op with another (#2169 shape). The tombstone keeps
- * the removal audible in both channels: `tsc` (input type `never`) and the
- * parse (the prescription itself).
- */
- activationEvents: retiredKey(ACTIVATION_EVENTS_RETIRED),
-
- /**
- * Configuration overrides for the plugin
- */
- config: z.record(z.string(), z.unknown()).optional()
- .describe('Runtime configuration overrides'),
-
- /**
- * Loading priority (lower = higher priority)
- */
- priority: z.number().int().min(0).default(100)
- .describe('Loading priority (lower is higher)'),
-
- /**
- * Whether to enable sandboxing for this dynamically loaded plugin
- */
- sandbox: z.boolean().default(false)
- .describe('Run in an isolated sandbox'),
-
- /**
- * Timeout for the load operation in milliseconds
- */
- timeout: z.number().int().min(1000).default(60000)
- .describe('Maximum time to complete loading in ms'),
-}).describe('Request to dynamically load a plugin at runtime'));
-
-/**
- * Dynamic Unload Request
- * Request to unload a plugin at runtime
- */
-export const DynamicUnloadRequestSchema = lazySchema(() => z.object({
- /**
- * Plugin identifier to unload
- */
- pluginId: z.string().describe('Plugin to unload'),
-
- /**
- * Unload strategy
- */
- strategy: z.enum([
- 'graceful', // Wait for in-flight requests, then unload
- 'forceful', // Unload immediately, cancel pending work
- 'drain', // Stop accepting new work, finish existing, then unload
- ]).default('graceful').describe('How to handle in-flight work during unload'),
-
- /**
- * Timeout for the unload operation in milliseconds
- */
- timeout: z.number().int().min(1000).default(30000)
- .describe('Maximum time to complete unloading in ms'),
-
- /**
- * Whether to remove cached artifacts
- */
- cleanupCache: z.boolean().default(false)
- .describe('Remove cached code and assets after unload'),
-
- /**
- * Action for dependents: plugins that depend on this one
- */
- dependentAction: z.enum([
- 'cascade', // Also unload dependent plugins
- 'warn', // Warn about dependents but proceed
- 'block', // Block unload if dependents exist
- ]).default('block').describe('How to handle plugins that depend on this one'),
-}).describe('Request to dynamically unload a plugin at runtime'));
-
-/**
- * Dynamic Plugin Operation Result
- * Result of a dynamic load/unload/reload operation
- */
-export const DynamicPluginResultSchema = lazySchema(() => z.object({
- /**
- * Whether the operation succeeded
- */
- success: z.boolean(),
-
- /**
- * The operation that was performed
- */
- operation: DynamicPluginOperationSchema,
-
- /**
- * Plugin identifier
- */
- pluginId: z.string(),
-
- /**
- * Operation duration in milliseconds
- */
- durationMs: z.number().int().min(0).optional(),
-
- /**
- * Resulting plugin version (for load/reload)
- */
- version: z.string().optional(),
-
- /**
- * Error details if operation failed
- */
- error: z.object({
- code: z.string().describe('Machine-readable error code'),
- message: z.string().describe('Human-readable error message'),
- details: z.record(z.string(), z.unknown()).optional(),
- }).optional(),
-
- /**
- * Warnings (e.g., dependents affected)
- */
- warnings: z.array(z.string()).optional(),
-}).describe('Result of a dynamic plugin operation'));
-
-/**
- * REMOVED — the plugin DISCOVERY + DYNAMIC-LOADING config surface (#3896 follow-up).
- *
- * `DynamicLoadingConfigSchema`, `PluginDiscoveryConfigSchema` and
- * `PluginDiscoverySourceSchema` declared a plugin sandboxing / integrity /
- * source-allow-list / approval-before-load control set. None of it was ever
- * composed into a parent schema, exported from the package root, or read by any
- * runtime: the three schemas were an ISLAND, reachable only from their own
- * round-trip tests — yet still published into `json-schema/` and the authorable
- * key surface, where an author (very often an AI, ADR-0033) would read them as a
- * capability this platform has.
- *
- * That is the ADR-0049 false-compliance shape, and the precedent for a
- * SAFETY-shaped one is to REMOVE rather than mark dead: `tool.requiresConfirmation`
- * was pruned in #3715 for exactly this reason — "unenforced on every path, so it
- * was false compliance, not merely dead".
- *
- * No `retiredKey()` tombstones. A tombstone earns its keep by making the removal
- * audible at a parse the author actually reaches, and NOTHING parses these
- * schemas — a prescription nobody can receive is noise, and the silent-strip the
- * key-vanish guard protects against was already these keys' permanent condition,
- * not something this change introduces. The guard's baseline entries are dropped
- * deliberately in this PR instead; see the changeset.
- *
- * Rebuilding this surface is a design job, not a schema job: write the runtime
- * first, then declare only what it enforces.
- */
-
-// Export types
-export type DynamicPluginOperation = z.infer;
-export type PluginSource = z.infer;
-export type DynamicLoadRequest = z.infer;
-export type DynamicUnloadRequest = z.infer;
-export type DynamicPluginResult = z.infer;
-
-// Export input types for schemas with defaults
-export type DynamicLoadRequestInput = z.input;
-export type DynamicUnloadRequestInput = z.input;
diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts
index 8591f1c010..d36ade59d1 100644
--- a/packages/spec/src/migrations/registry.ts
+++ b/packages/spec/src/migrations/registry.ts
@@ -751,6 +751,22 @@ const step17: MigrationStep = {
+ 'key is rejected by the strict manifest parse with its own guidance prescription, and '
+ 'the orphaned `ActivationEventSchema` def is removed with them. Behaviour is '
+ 'byte-identical: eager activation was always the only behaviour.\n\n'
+ + 'That kernel-side tombstone was then SUPERSEDED inside the same unreleased major by '
+ + '#4834, which finished the enforce-or-remove pass one level up: the entire '
+ + '`plugin-runtime.zod` family — `DynamicLoadRequest`, `DynamicUnloadRequest`, '
+ + '`DynamicPluginResult`, `PluginSource`, `DynamicPluginOperation` — is removed, because '
+ + 'the "Dynamic Loading" capability it described (runtime load / unload / reload without '
+ + 'a kernel restart, with sandboxing, integrity hashes, drain strategies and '
+ + 'dependent-cascade policy) has no server anywhere: no runtime in objectstack, cloud or '
+ + 'objectui ever received one of these requests or produced one of these results. #3896 '
+ + 'had suspended the call on these five deliberately — "operation contracts, not security '
+ + 'promises" — in a changeset paragraph no issue carried; #4834 is that decision, '
+ + 'answered REMOVE. So a v16 author who wrote `activationEvents` inside a '
+ + '`DynamicLoadRequest` value does not delete a key: the whole value has no shape and no '
+ + 'recipient, and importing `DynamicLoadRequestSchema` at all is TS2305 in v17. The '
+ + 'studio half of the `activationEvents` retirement is untouched and still rejects the '
+ + 'key with its own prescription — `defineStudioPlugin` remains a live authoring surface. '
+ + 'Behaviour is again byte-identical: nothing ever executed a dynamic plugin operation.\n\n'
+ "Finally it removes the script-body capability token 'crypto.hash' (#4391). Four layers "
+ 'declared it — the `HookBodyCapability` enum, the doc table beside it, the CLI extractor '
+ 'and `ScriptContext.crypto.hash` — and none implemented it: `installCtx` wired only '
@@ -1313,15 +1329,72 @@ const step17: MigrationStep = {
+ '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.',
+ + '(#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` below). The studio half of this entry is '
+ + 'unaffected and still enforced by the strict manifest parse.',
acceptanceCriteria:
- 'No `DynamicLoadRequest` or `defineStudioPlugin` input authors `activationEvents` — '
- + 'authoring it is a tsc error (`never` on the kernel side; an unknown key on the '
- + 'strict studio manifest) and a parse error carrying the prescription on both. No '
+ '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.',
},
+ {
+ id: 'plugin-runtime-family-retired',
+ surface:
+ 'kernel.dynamicLoadRequest / kernel.dynamicUnloadRequest / kernel.dynamicPluginResult '
+ + '/ kernel.pluginSource / kernel.dynamicPluginOperation',
+ replacement:
+ '(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)',
+ reason:
+ 'The five schemas declared the "Dynamic Loading" capability — runtime load / unload / '
+ + 'reload of plugins without a kernel restart, with sandboxing, integrity hashes, '
+ + 'drain strategies and dependent-cascade policy — and NOTHING implemented it. A '
+ + 'bare-name scan of objectstack, cloud and objectui found zero references outside '
+ + "this package's own declaration, its unit tests and the generated artifacts: no "
+ + 'runtime ever received a `DynamicLoadRequest`, performed a load/unload, or produced '
+ + 'a `DynamicPluginResult`. That is the ADR-0049 false-compliance shape at its most '
+ + 'inviting to an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` in the '
+ + 'published IDE bundle as proof the platform hot-loads plugins and constructs a '
+ + 'request that parses clean and is received by nobody (#3950: an exported schema '
+ + 'with no consumer is read as a capability). The #3896 follow-up removed this '
+ + "module's discovery/sandbox config island and left these five in place explicitly — "
+ + '"operation contracts, not security promises; the enforce-or-remove call on them is '
+ + 'a design decision rather than a correction" — but that suspension lived only in a '
+ + 'changeset paragraph with no issue carrying it. #4834 is that decision, answered '
+ + 'REMOVE. `experimental` was considered and rejected: it is only `.describe()` prose '
+ + 'and cannot stop an import, the weakest of the three ADR-0049 channels. None of the '
+ + 'five is stored metadata — they are root request/result payload shapes embedded in '
+ + 'no parent schema and parsed against no metadata document — so no `sys_metadata` '
+ + 'row can carry one and there is no source for the D2 chain to rewrite; this entry '
+ + 'is the D3 record. The removal also subsumes the kernel half of '
+ + '`plugin-activation-events-retired` (#4657): that tombstone goes with the shape '
+ + 'that carried it. ADR-0049, #4834.',
+ acceptanceCriteria:
+ 'No code imports `DynamicLoadRequestSchema`, `DynamicUnloadRequestSchema`, '
+ + '`DynamicPluginResultSchema`, `PluginSourceSchema`, `DynamicPluginOperationSchema` '
+ + 'or any of their type aliases (`DynamicLoadRequest`, `DynamicUnloadRequest`, '
+ + '`DynamicPluginResult`, `PluginSource`, `DynamicPluginOperation`, '
+ + '`DynamicLoadRequestInput`, `DynamicUnloadRequestInput`) from '
+ + '`@objectstack/spec` or `@objectstack/spec/kernel` — every one is TS2305 after '
+ + 'upgrade, on every public entry (pinned by symbol identity in '
+ + '`plugin-runtime-retirement.test.ts`). Nothing regresses at runtime, because '
+ + 'nothing called anything: a caller that believed it was hot-loading a plugin was '
+ + 'already only building an object. Boot-time composition through `defineStack` is '
+ + 'unchanged.',
+ },
],
};