Skip to content

Commit 62f8017

Browse files
authored
refactor(spec)!: remove the plugin sandboxing / integrity / approval config that never existed (#3950)
Follow-up to #3896/#3945. Found while building the empty-state gate: `allowedSources` documented `[]` as admitting every source, and checking who enforced that turned up nobody. `DynamicLoadingConfigSchema`, `PluginDiscoveryConfigSchema` and `PluginDiscoverySourceSchema` promised sandboxing (`defaultSandbox`), integrity verification (`requireIntegrity`), source allow-listing (`allowedSources`) and admin approval (`discovery.requireApproval`) for dynamically loaded plugins. None of it was ever wired to anything — the three schemas were an island, referenced only by their own round-trip tests, yet published into json-schema/ and the authorable key surface where an author reads them as capabilities the platform has. That is ADR-0049 false compliance in its security shape, and the precedent (`tool.requiresConfirmation`, #3715) is to remove rather than mark dead. No tombstones: nothing ever parsed these schemas, so a retiredKey() prescription could never be delivered — the manifest and authorable-surface baselines are dropped as the deliberate removal both files document, with a block comment in the source recording the reasoning. Breaking in the narrow sense: the types were importable via the published ./kernel subpath. Nothing imported them; folds into the unreleased 17.0.0. `ActivationEventSchema` (7 real consumers) stays. spec: 263 files / 6856 tests green, tsc clean, all six spec gates green, reference docs regenerated.
1 parent a225ef5 commit 62f8017

8 files changed

Lines changed: 85 additions & 386 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
refactor(spec)!: remove the plugin sandboxing / integrity / approval config that never existed (#3896 follow-up)
6+
7+
`DynamicLoadingConfigSchema`, `PluginDiscoveryConfigSchema` and
8+
`PluginDiscoverySourceSchema` declared a plugin security control set —
9+
`defaultSandbox`, `requireIntegrity`, `allowedSources`, and discovery's
10+
`requireApproval` ("require admin approval before loading discovered plugins").
11+
12+
**None of it was ever wired to anything.** The three schemas were an island: not
13+
composed into any parent schema, not read by any runtime, referenced only by
14+
their own round-trip tests. They were nonetheless published into `json-schema/`
15+
and the authorable key surface, where an author — very often an AI (ADR-0033) —
16+
would read them as capabilities this platform has. A reader of the spec could
17+
reasonably conclude ObjectStack sandboxes dynamically loaded plugins. It does
18+
not.
19+
20+
That is the ADR-0049 false-compliance shape, and the precedent for a
21+
SAFETY-shaped instance is to remove rather than mark dead:
22+
`tool.requiresConfirmation` was pruned in #3715 because it was "unenforced on
23+
every path, so it was false compliance, not merely dead". This is the same case,
24+
one layer up.
25+
26+
Found while building the empty-state gate (#3945): `allowedSources` documented
27+
`[]` as admitting every source, and checking who enforced that turned up nobody.
28+
29+
**No `retiredKey()` tombstones, deliberately.** A tombstone earns its keep by
30+
making a removal audible at a parse the author actually reaches — and nothing
31+
parses these schemas, so the prescription could never be delivered. The
32+
silent-strip that the key-vanish guard exists to prevent was already these keys'
33+
permanent condition: writing one has always been a no-op, because no parent
34+
schema ever accepted them. The guard's baseline entries in
35+
`json-schema.manifest.json` and `authorable-surface.json` are therefore dropped
36+
in this PR as the deliberate removal both files document as the legitimate path,
37+
rather than tombstoning 15 keys nobody could have successfully authored.
38+
39+
**Breaking, in the narrow sense.** `packages/spec/src/kernel/index.ts`
40+
re-exports this module with `export *`, and `./kernel` is a published subpath, so
41+
`DynamicLoadingConfig`, `PluginDiscoveryConfig`, `PluginDiscoverySource` and
42+
their schemas were importable as types. Nothing in this repo imported them.
43+
Marked `major` because removing a public export is breaking regardless of use;
44+
in practice it folds into the unreleased 17.0.0.
45+
46+
The rest of `plugin-runtime.zod.ts` is untouched — including
47+
`ActivationEventSchema`, the one export in the file with real consumers. Note
48+
that the remainder (`DynamicLoadRequest`, `DynamicUnloadRequest`,
49+
`DynamicPluginResult`, `PluginSource`, `DynamicPluginOperation`) also has no
50+
runtime consumer today; it is left in place because those are operation
51+
contracts, not security promises, and the enforce-or-remove call on them is a
52+
design decision rather than a correction.
53+
54+
**Rebuilding this surface is a design job, not a schema job**: write the runtime
55+
first, then declare only what it enforces.

content/docs/references/kernel/plugin-runtime.mdx

Lines changed: 2 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ This protocol enables:
3838
## TypeScript Usage
3939

4040
```typescript
41-
import { DynamicLoadRequest, DynamicLoadingConfig, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginDiscoveryConfig, PluginDiscoverySource, PluginSource } from '@objectstack/spec/kernel';
42-
import type { DynamicLoadRequest, DynamicLoadingConfig, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginDiscoveryConfig, PluginDiscoverySource, PluginSource } from '@objectstack/spec/kernel';
41+
import { DynamicLoadRequest, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginSource } from '@objectstack/spec/kernel';
42+
import type { DynamicLoadRequest, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginSource } from '@objectstack/spec/kernel';
4343

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

6666

67-
---
68-
69-
## DynamicLoadingConfig
70-
71-
Dynamic plugin loading subsystem configuration
72-
73-
### Properties
74-
75-
| Property | Type | Required | Description |
76-
| :--- | :--- | :--- | :--- |
77-
| **enabled** | `boolean` || Enable runtime load/unload of plugins |
78-
| **maxDynamicPlugins** | `integer` || Upper limit on runtime-loaded plugins |
79-
| **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 |
80-
| **defaultSandbox** | `boolean` || Sandbox dynamically loaded plugins by default |
81-
| **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 |
82-
| **requireIntegrity** | `boolean` || Require integrity hash verification for remote sources |
83-
| **operationTimeout** | `integer` || Default timeout for load/unload operations in ms |
84-
85-
8667
---
8768

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

136117

137-
---
138-
139-
## PluginDiscoveryConfig
140-
141-
Runtime plugin discovery configuration
142-
143-
### Properties
144-
145-
| Property | Type | Required | Description |
146-
| :--- | :--- | :--- | :--- |
147-
| **enabled** | `boolean` || |
148-
| **sources** | `{ type: Enum<'registry' \| 'npm' \| 'directory' \| 'url'>; endpoint: string; pollInterval: integer; filter?: object }[]` || |
149-
| **autoLoad** | `boolean` || Automatically load newly discovered plugins |
150-
| **requireApproval** | `boolean` || Require admin approval before loading discovered plugins |
151-
152-
153-
---
154-
155-
## PluginDiscoverySource
156-
157-
Source for runtime plugin discovery
158-
159-
### Properties
160-
161-
| Property | Type | Required | Description |
162-
| :--- | :--- | :--- | :--- |
163-
| **type** | `Enum<'registry' \| 'npm' \| 'directory' \| 'url'>` || Discovery source type |
164-
| **endpoint** | `string` || Registry URL, directory path, or manifest URL |
165-
| **pollInterval** | `integer` || How often to re-scan for new plugins (0 = manual) |
166-
| **filter** | `{ tags?: string[]; vendors?: string[]; minTrustLevel?: Enum<'verified' \| 'trusted' \| 'community' \| 'untrusted'> }` | optional | |
167-
168-
169118
---
170119

171120
## PluginSource

packages/spec/api-surface.json

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,9 +1402,6 @@
14021402
"DynamicLoadRequest (type)",
14031403
"DynamicLoadRequestInput (type)",
14041404
"DynamicLoadRequestSchema (const)",
1405-
"DynamicLoadingConfig (type)",
1406-
"DynamicLoadingConfigInput (type)",
1407-
"DynamicLoadingConfigSchema (const)",
14081405
"DynamicPluginOperation (type)",
14091406
"DynamicPluginOperationSchema (const)",
14101407
"DynamicPluginResult (type)",
@@ -1630,10 +1627,6 @@
16301627
"PluginDependencyResolutionResultSchema (const)",
16311628
"PluginDependencyResolutionSchema (const)",
16321629
"PluginDependencySchema (const)",
1633-
"PluginDiscoveryConfig (type)",
1634-
"PluginDiscoveryConfigSchema (const)",
1635-
"PluginDiscoverySource (type)",
1636-
"PluginDiscoverySourceSchema (const)",
16371630
"PluginDynamicImport (type)",
16381631
"PluginDynamicImportSchema (const)",
16391632
"PluginEngines (type)",

packages/spec/authorable-surface.json

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"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.",
2+
"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.",
33
"keys": [
44
"ai/AIKnowledge:indexes",
55
"ai/AIKnowledge:sources",
@@ -4631,13 +4631,6 @@
46314631
"kernel/DynamicLoadRequest:sandbox",
46324632
"kernel/DynamicLoadRequest:source",
46334633
"kernel/DynamicLoadRequest:timeout",
4634-
"kernel/DynamicLoadingConfig:allowedSources",
4635-
"kernel/DynamicLoadingConfig:defaultSandbox",
4636-
"kernel/DynamicLoadingConfig:discovery",
4637-
"kernel/DynamicLoadingConfig:enabled",
4638-
"kernel/DynamicLoadingConfig:maxDynamicPlugins",
4639-
"kernel/DynamicLoadingConfig:operationTimeout",
4640-
"kernel/DynamicLoadingConfig:requireIntegrity",
46414634
"kernel/DynamicPluginResult:durationMs",
46424635
"kernel/DynamicPluginResult:error",
46434636
"kernel/DynamicPluginResult:operation",
@@ -5174,14 +5167,6 @@
51745167
"kernel/PluginDependencyResolutionResult:resolved",
51755168
"kernel/PluginDependencyResolutionResult:success",
51765169
"kernel/PluginDependencyResolutionResult:warnings",
5177-
"kernel/PluginDiscoveryConfig:autoLoad",
5178-
"kernel/PluginDiscoveryConfig:enabled",
5179-
"kernel/PluginDiscoveryConfig:requireApproval",
5180-
"kernel/PluginDiscoveryConfig:sources",
5181-
"kernel/PluginDiscoverySource:endpoint",
5182-
"kernel/PluginDiscoverySource:filter",
5183-
"kernel/PluginDiscoverySource:pollInterval",
5184-
"kernel/PluginDiscoverySource:type",
51855170
"kernel/PluginDynamicImport:enabled",
51865171
"kernel/PluginDynamicImport:mode",
51875172
"kernel/PluginDynamicImport:prefetch",

packages/spec/json-schema.manifest.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"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.",
2+
"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.",
33
"schemas": [
44
"ai/AIKnowledge",
55
"ai/AIModelConfig",
@@ -970,7 +970,6 @@
970970
"kernel/DisablePackageResponse",
971971
"kernel/DistributedStateConfig",
972972
"kernel/DynamicLoadRequest",
973-
"kernel/DynamicLoadingConfig",
974973
"kernel/DynamicPluginOperation",
975974
"kernel/DynamicPluginResult",
976975
"kernel/DynamicUnloadRequest",
@@ -1075,8 +1074,6 @@
10751074
"kernel/PluginDependency",
10761075
"kernel/PluginDependencyResolution",
10771076
"kernel/PluginDependencyResolutionResult",
1078-
"kernel/PluginDiscoveryConfig",
1079-
"kernel/PluginDiscoverySource",
10801077
"kernel/PluginDynamicImport",
10811078
"kernel/PluginEngines",
10821079
"kernel/PluginErrorEvent",

packages/spec/scripts/liveness/empty-state-registry.mts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
// "restricts" something — carries opposite meanings when it is empty:
1717
//
1818
// object.apiMethods `undefined` = unrestricted, `[]` = deny-all (closed on empty)
19-
// allowedSources "(empty = all allowed)" (open on empty)
19+
// allowedSources "(empty = all allowed)" — REMOVED with its schema (#3896 follow-up)
2020
// sharing criteria was match-all, now matches nothing (closed on empty)
2121
//
2222
// Nothing in the metadata distinguishes them. A maintainer knows by memory which
@@ -98,14 +98,6 @@ export const EMPTY_STATE_REGISTRY: EmptyStateEntry[] = [
9898
'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.',
9999
evidence: 'packages/spec/src/data/api-derivation.ts',
100100
},
101-
{
102-
file: 'packages/spec/src/kernel/plugin-runtime.zod.ts',
103-
property: 'allowedSources',
104-
semantics: 'open',
105-
rationale:
106-
'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.',
107-
evidence: 'packages/spec/src/kernel/plugin-runtime.zod.ts',
108-
},
109101
{
110102
file: 'packages/spec/src/security/sharing.zod.ts',
111103
property: 'condition',

0 commit comments

Comments
 (0)