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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .changeset/remove-unenforced-plugin-loading-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
"@objectstack/spec": major
---

refactor(spec)!: remove the plugin sandboxing / integrity / approval config that never existed (#3896 follow-up)

`DynamicLoadingConfigSchema`, `PluginDiscoveryConfigSchema` and
`PluginDiscoverySourceSchema` declared a plugin security control set —
`defaultSandbox`, `requireIntegrity`, `allowedSources`, and discovery's
`requireApproval` ("require admin approval before loading discovered plugins").

**None of it was ever wired to anything.** The three schemas were an island: not
composed into any parent schema, not read by any runtime, referenced only by
their own round-trip tests. They were nonetheless published into `json-schema/`
and the authorable key surface, where an author — very often an AI (ADR-0033) —
would read them as capabilities this platform has. A reader of the spec could
reasonably conclude ObjectStack sandboxes dynamically loaded plugins. It does
not.

That is the ADR-0049 false-compliance shape, and the precedent for a
SAFETY-shaped instance is to remove rather than mark dead:
`tool.requiresConfirmation` was pruned in #3715 because it was "unenforced on
every path, so it was false compliance, not merely dead". This is the same case,
one layer up.

Found while building the empty-state gate (#3945): `allowedSources` documented
`[]` as admitting every source, and checking who enforced that turned up nobody.

**No `retiredKey()` tombstones, deliberately.** A tombstone earns its keep by
making a removal audible at a parse the author actually reaches — and nothing
parses these schemas, so the prescription could never be delivered. The
silent-strip that the key-vanish guard exists to prevent was already these keys'
permanent condition: writing one has always been a no-op, because no parent
schema ever accepted them. The guard's baseline entries in
`json-schema.manifest.json` and `authorable-surface.json` are therefore dropped
in this PR as the deliberate removal both files document as the legitimate path,
rather than tombstoning 15 keys nobody could have successfully authored.

**Breaking, in the narrow sense.** `packages/spec/src/kernel/index.ts`
re-exports this module with `export *`, and `./kernel` is a published subpath, so
`DynamicLoadingConfig`, `PluginDiscoveryConfig`, `PluginDiscoverySource` and
their schemas were importable as types. Nothing in this repo imported them.
Marked `major` because removing a public export is breaking regardless of use;
in practice it folds into the unreleased 17.0.0.

The rest of `plugin-runtime.zod.ts` is untouched — including
`ActivationEventSchema`, the one export in the file with real consumers. Note
that the remainder (`DynamicLoadRequest`, `DynamicUnloadRequest`,
`DynamicPluginResult`, `PluginSource`, `DynamicPluginOperation`) 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.

**Rebuilding this surface is a design job, not a schema job**: write the runtime
first, then declare only what it enforces.
55 changes: 2 additions & 53 deletions content/docs/references/kernel/plugin-runtime.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ This protocol enables:
## TypeScript Usage

```typescript
import { DynamicLoadRequest, DynamicLoadingConfig, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginDiscoveryConfig, PluginDiscoverySource, PluginSource } from '@objectstack/spec/kernel';
import type { DynamicLoadRequest, DynamicLoadingConfig, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginDiscoveryConfig, PluginDiscoverySource, PluginSource } from '@objectstack/spec/kernel';
import { DynamicLoadRequest, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginSource } from '@objectstack/spec/kernel';
import type { DynamicLoadRequest, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginSource } from '@objectstack/spec/kernel';

// Validate data
const result = DynamicLoadRequest.parse(data);
Expand All @@ -64,25 +64,6 @@ Request to dynamically load a plugin at runtime
| **timeout** | `integer` | ✅ | Maximum time to complete loading in ms |


---

## DynamicLoadingConfig

Dynamic plugin loading subsystem configuration

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **enabled** | `boolean` | ✅ | Enable runtime load/unload of plugins |
| **maxDynamicPlugins** | `integer` | ✅ | Upper limit on runtime-loaded plugins |
| **discovery** | `{ enabled: boolean; sources: { type: Enum<'registry' \| 'npm' \| 'directory' \| 'url'>; endpoint: string; pollInterval: integer; filter?: object }[]; autoLoad: boolean; requireApproval: boolean }` | optional | Runtime plugin discovery configuration |
| **defaultSandbox** | `boolean` | ✅ | Sandbox dynamically loaded plugins by default |
| **allowedSources** | `Enum<'npm' \| 'local' \| 'url' \| 'registry' \| 'git'>[]` | optional | [EXPERIMENTAL — not enforced] Restrict which plugin source types are permitted: undefined = any source, [] = deny-all, a subset = exactly those types |
| **requireIntegrity** | `boolean` | ✅ | Require integrity hash verification for remote sources |
| **operationTimeout** | `integer` | ✅ | Default timeout for load/unload operations in ms |


---

## DynamicPluginOperation
Expand Down Expand Up @@ -134,38 +115,6 @@ Request to dynamically unload a plugin at runtime
| **dependentAction** | `Enum<'cascade' \| 'warn' \| 'block'>` | ✅ | How to handle plugins that depend on this one |


---

## PluginDiscoveryConfig

Runtime plugin discovery configuration

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **enabled** | `boolean` | ✅ | |
| **sources** | `{ type: Enum<'registry' \| 'npm' \| 'directory' \| 'url'>; endpoint: string; pollInterval: integer; filter?: object }[]` | ✅ | |
| **autoLoad** | `boolean` | ✅ | Automatically load newly discovered plugins |
| **requireApproval** | `boolean` | ✅ | Require admin approval before loading discovered plugins |


---

## PluginDiscoverySource

Source for runtime plugin discovery

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `Enum<'registry' \| 'npm' \| 'directory' \| 'url'>` | ✅ | Discovery source type |
| **endpoint** | `string` | ✅ | Registry URL, directory path, or manifest URL |
| **pollInterval** | `integer` | ✅ | How often to re-scan for new plugins (0 = manual) |
| **filter** | `{ tags?: string[]; vendors?: string[]; minTrustLevel?: Enum<'verified' \| 'trusted' \| 'community' \| 'untrusted'> }` | optional | |


---

## PluginSource
Expand Down
7 changes: 0 additions & 7 deletions packages/spec/api-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -1402,9 +1402,6 @@
"DynamicLoadRequest (type)",
"DynamicLoadRequestInput (type)",
"DynamicLoadRequestSchema (const)",
"DynamicLoadingConfig (type)",
"DynamicLoadingConfigInput (type)",
"DynamicLoadingConfigSchema (const)",
"DynamicPluginOperation (type)",
"DynamicPluginOperationSchema (const)",
"DynamicPluginResult (type)",
Expand Down Expand Up @@ -1630,10 +1627,6 @@
"PluginDependencyResolutionResultSchema (const)",
"PluginDependencyResolutionSchema (const)",
"PluginDependencySchema (const)",
"PluginDiscoveryConfig (type)",
"PluginDiscoveryConfigSchema (const)",
"PluginDiscoverySource (type)",
"PluginDiscoverySourceSchema (const)",
"PluginDynamicImport (type)",
"PluginDynamicImportSchema (const)",
"PluginEngines (type)",
Expand Down
17 changes: 1 addition & 16 deletions packages/spec/authorable-surface.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"description": "Ratchet of every AUTHORABLE key in the spec what a metadata author may write, which for this platform IS the third-party API. Auto-updated on additions (commit the change). A key that disappears without a tombstone fails gen:schema, because these schemas are not .strict() and Zod would silently strip it. \"[RETIRED]\" marks a tombstoned key that still rejects with an upgrade prescription. See #3855, ADR-0059 §5.",
"description": "Ratchet of every AUTHORABLE key in the spec \u2014 what a metadata author may write, which for this platform IS the third-party API. Auto-updated on additions (commit the change). A key that disappears without a tombstone fails gen:schema, because these schemas are not .strict() and Zod would silently strip it. \"[RETIRED]\" marks a tombstoned key that still rejects with an upgrade prescription. See #3855, ADR-0059 \u00a75.",
"keys": [
"ai/AIKnowledge:indexes",
"ai/AIKnowledge:sources",
Expand Down Expand Up @@ -4631,13 +4631,6 @@
"kernel/DynamicLoadRequest:sandbox",
"kernel/DynamicLoadRequest:source",
"kernel/DynamicLoadRequest:timeout",
"kernel/DynamicLoadingConfig:allowedSources",
"kernel/DynamicLoadingConfig:defaultSandbox",
"kernel/DynamicLoadingConfig:discovery",
"kernel/DynamicLoadingConfig:enabled",
"kernel/DynamicLoadingConfig:maxDynamicPlugins",
"kernel/DynamicLoadingConfig:operationTimeout",
"kernel/DynamicLoadingConfig:requireIntegrity",
"kernel/DynamicPluginResult:durationMs",
"kernel/DynamicPluginResult:error",
"kernel/DynamicPluginResult:operation",
Expand Down Expand Up @@ -5174,14 +5167,6 @@
"kernel/PluginDependencyResolutionResult:resolved",
"kernel/PluginDependencyResolutionResult:success",
"kernel/PluginDependencyResolutionResult:warnings",
"kernel/PluginDiscoveryConfig:autoLoad",
"kernel/PluginDiscoveryConfig:enabled",
"kernel/PluginDiscoveryConfig:requireApproval",
"kernel/PluginDiscoveryConfig:sources",
"kernel/PluginDiscoverySource:endpoint",
"kernel/PluginDiscoverySource:filter",
"kernel/PluginDiscoverySource:pollInterval",
"kernel/PluginDiscoverySource:type",
"kernel/PluginDynamicImport:enabled",
"kernel/PluginDynamicImport:mode",
"kernel/PluginDynamicImport:prefetch",
Expand Down
5 changes: 1 addition & 4 deletions packages/spec/json-schema.manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"description": "Ratchet manifest of every JSON Schema emitted by scripts/build-schemas.ts. Auto-appended when new schemas are added (commit the change). A listed schema that a build no longer emits fails gen:schema remove a key ONLY for a deliberate retirement. See #2978.",
"description": "Ratchet manifest of every JSON Schema emitted by scripts/build-schemas.ts. Auto-appended when new schemas are added (commit the change). A listed schema that a build no longer emits fails gen:schema \u2014 remove a key ONLY for a deliberate retirement. See #2978.",
"schemas": [
"ai/AIKnowledge",
"ai/AIModelConfig",
Expand Down Expand Up @@ -970,7 +970,6 @@
"kernel/DisablePackageResponse",
"kernel/DistributedStateConfig",
"kernel/DynamicLoadRequest",
"kernel/DynamicLoadingConfig",
"kernel/DynamicPluginOperation",
"kernel/DynamicPluginResult",
"kernel/DynamicUnloadRequest",
Expand Down Expand Up @@ -1075,8 +1074,6 @@
"kernel/PluginDependency",
"kernel/PluginDependencyResolution",
"kernel/PluginDependencyResolutionResult",
"kernel/PluginDiscoveryConfig",
"kernel/PluginDiscoverySource",
"kernel/PluginDynamicImport",
"kernel/PluginEngines",
"kernel/PluginErrorEvent",
Expand Down
10 changes: 1 addition & 9 deletions packages/spec/scripts/liveness/empty-state-registry.mts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
// "restricts" something — carries opposite meanings when it is empty:
//
// object.apiMethods `undefined` = unrestricted, `[]` = deny-all (closed on empty)
// allowedSources "(empty = all allowed)" (open on empty)
// allowedSources "(empty = all allowed)" — REMOVED with its schema (#3896 follow-up)
// sharing criteria was match-all, now matches nothing (closed on empty)
//
// Nothing in the metadata distinguishes them. A maintainer knows by memory which
Expand Down Expand Up @@ -98,14 +98,6 @@ export const EMPTY_STATE_REGISTRY: EmptyStateEntry[] = [
'Deliberate default-open, and the reference example of stating it fully: `undefined` = unrestricted (an object is API-exposed unless it opts into a whitelist — exposure is the CRUD default), `[]` = deny-all, a subset = the derived closure (#3391/#3543). The empty ARRAY is closed; only ABSENCE is open. That two-part contract is what makes it safe to author against, and object.zod.ts additionally emits a ⚠ in its describe text when stripping leaves the whitelist empty.',
evidence: 'packages/spec/src/data/api-derivation.ts',
},
{
file: 'packages/spec/src/kernel/plugin-runtime.zod.ts',
property: 'allowedSources',
semantics: 'open',
rationale:
'A source allow-list for dynamically loaded plugins — a supply-chain gate. Corrected to the `apiMethods` three-state: absence stays open (no policy declared, matching how every other config default behaves and how the runtime behaves today), but `[]` now DENIES instead of admitting everything. The vacuous allow-list was the whole defect: emptiness was both the likeliest authoring slip and the widest grant. Registered `open` rather than `closed` because absence really is permissive here — evading the scanner with softer wording would be the same silence this gate exists to break. NOTE it has no runtime consumer at all: the whole DynamicLoadingConfig block (with `requireIntegrity`, `defaultSandbox`) is declared-but-unenforced, ADR-0049 false compliance, tracked separately — an unimplemented gate is read as the specification by whoever implements it.',
evidence: 'packages/spec/src/kernel/plugin-runtime.zod.ts',
},
{
file: 'packages/spec/src/security/sharing.zod.ts',
property: 'condition',
Expand Down
Loading
Loading