From 851ded92dc17bd6e909b0fabcdb8bfd265969644 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 12:29:57 +0000 Subject: [PATCH 1/3] refactor(spec)!: remove the plugin sandboxing/integrity/approval config that never existed (#3896) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) — reads them as capabilities this platform has. Someone reading 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". 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 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 baseline entries in json-schema.manifest.json and authorable-surface.json are dropped here 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: kernel/index.ts re-exports this module with `export *` and ./kernel is a published subpath, so the types were importable. Nothing in this repo imported them; it folds into the unreleased 17.0.0. The rest of the file is untouched, including ActivationEventSchema — the one export in it with real consumers. The remainder (DynamicLoadRequest, DynamicUnloadRequest, DynamicPluginResult, PluginSource, DynamicPluginOperation) also has no runtime consumer, but those are operation contracts rather than security promises, so the enforce-or-remove call on them is a design decision, not a correction. spec: 263 files / 6856 tests green, tsc clean, all six generated-artifact and liveness gates pass. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QuViRSR1j6GJjf9qGbnqFX --- ...remove-unenforced-plugin-loading-config.md | 55 ++++++ .../docs/references/kernel/plugin-runtime.mdx | 55 +----- packages/spec/authorable-surface.json | 17 +- packages/spec/json-schema.manifest.json | 5 +- .../scripts/liveness/empty-state-registry.mts | 10 +- .../spec/src/kernel/plugin-runtime.test.ts | 163 ------------------ .../spec/src/kernel/plugin-runtime.zod.ts | 159 +++-------------- 7 files changed, 85 insertions(+), 379 deletions(-) create mode 100644 .changeset/remove-unenforced-plugin-loading-config.md diff --git a/.changeset/remove-unenforced-plugin-loading-config.md b/.changeset/remove-unenforced-plugin-loading-config.md new file mode 100644 index 0000000000..cf54fb95de --- /dev/null +++ b/.changeset/remove-unenforced-plugin-loading-config.md @@ -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. diff --git a/content/docs/references/kernel/plugin-runtime.mdx b/content/docs/references/kernel/plugin-runtime.mdx index 46b5370f22..ac8e87c23b 100644 --- a/content/docs/references/kernel/plugin-runtime.mdx +++ b/content/docs/references/kernel/plugin-runtime.mdx @@ -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); @@ -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 @@ -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 diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index 364a491377..0530f97286 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -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", @@ -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", @@ -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", diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index 688ab8a625..a2e630920f 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -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", @@ -970,7 +970,6 @@ "kernel/DisablePackageResponse", "kernel/DistributedStateConfig", "kernel/DynamicLoadRequest", - "kernel/DynamicLoadingConfig", "kernel/DynamicPluginOperation", "kernel/DynamicPluginResult", "kernel/DynamicUnloadRequest", @@ -1075,8 +1074,6 @@ "kernel/PluginDependency", "kernel/PluginDependencyResolution", "kernel/PluginDependencyResolutionResult", - "kernel/PluginDiscoveryConfig", - "kernel/PluginDiscoverySource", "kernel/PluginDynamicImport", "kernel/PluginEngines", "kernel/PluginErrorEvent", diff --git a/packages/spec/scripts/liveness/empty-state-registry.mts b/packages/spec/scripts/liveness/empty-state-registry.mts index fca529705a..5a25f6ca50 100644 --- a/packages/spec/scripts/liveness/empty-state-registry.mts +++ b/packages/spec/scripts/liveness/empty-state-registry.mts @@ -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 @@ -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', diff --git a/packages/spec/src/kernel/plugin-runtime.test.ts b/packages/spec/src/kernel/plugin-runtime.test.ts index 1db765439b..aab3d714fd 100644 --- a/packages/spec/src/kernel/plugin-runtime.test.ts +++ b/packages/spec/src/kernel/plugin-runtime.test.ts @@ -6,9 +6,6 @@ import { DynamicLoadRequestSchema, DynamicUnloadRequestSchema, DynamicPluginResultSchema, - PluginDiscoverySourceSchema, - PluginDiscoveryConfigSchema, - DynamicLoadingConfigSchema, } from './plugin-runtime.zod'; describe('Plugin Runtime Management Protocol', () => { @@ -209,164 +206,4 @@ describe('Plugin Runtime Management Protocol', () => { }); }); - describe('PluginDiscoverySourceSchema', () => { - it('should accept registry discovery source', () => { - const source = { - type: 'registry', - endpoint: 'https://plugins.objectstack.dev/api/v1', - pollInterval: 300000, - filter: { - tags: ['analytics', 'reporting'], - minTrustLevel: 'trusted' as const, - }, - }; - const result = PluginDiscoverySourceSchema.parse(source); - expect(result.type).toBe('registry'); - expect(result.pollInterval).toBe(300000); - expect(result.filter?.tags).toHaveLength(2); - }); - - it('should accept directory discovery source', () => { - const source = { - type: 'directory', - endpoint: '/opt/objectstack/plugins', - }; - const result = PluginDiscoverySourceSchema.parse(source); - expect(result.type).toBe('directory'); - expect(result.pollInterval).toBe(0); // default - }); - }); - - describe('PluginDiscoveryConfigSchema', () => { - it('should apply defaults', () => { - const config = PluginDiscoveryConfigSchema.parse({}); - expect(config.enabled).toBe(false); - expect(config.sources).toEqual([]); - expect(config.autoLoad).toBe(false); - expect(config.requireApproval).toBe(true); - }); - - it('should accept full discovery config', () => { - const config = { - enabled: true, - sources: [ - { - type: 'registry' as const, - endpoint: 'https://plugins.objectstack.dev', - pollInterval: 60000, - }, - { - type: 'directory' as const, - endpoint: '/opt/plugins', - }, - ], - autoLoad: true, - requireApproval: false, - }; - const result = PluginDiscoveryConfigSchema.parse(config); - expect(result.sources).toHaveLength(2); - expect(result.autoLoad).toBe(true); - }); - }); - - describe('DynamicLoadingConfigSchema', () => { - it('should apply defaults', () => { - const config = DynamicLoadingConfigSchema.parse({}); - expect(config.enabled).toBe(false); - expect(config.maxDynamicPlugins).toBe(50); - expect(config.defaultSandbox).toBe(true); - expect(config.requireIntegrity).toBe(true); - expect(config.operationTimeout).toBe(60000); - }); - - it('should accept full dynamic loading config', () => { - const config = { - enabled: true, - maxDynamicPlugins: 100, - discovery: { - enabled: true, - sources: [ - { - type: 'registry' as const, - endpoint: 'https://plugins.objectstack.dev', - }, - ], - autoLoad: false, - requireApproval: true, - }, - defaultSandbox: true, - allowedSources: ['npm', 'registry'] as const, - requireIntegrity: true, - operationTimeout: 120000, - }; - const result = DynamicLoadingConfigSchema.parse(config); - expect(result.enabled).toBe(true); - expect(result.maxDynamicPlugins).toBe(100); - expect(result.discovery?.enabled).toBe(true); - expect(result.allowedSources).toHaveLength(2); - }); - }); - - describe('Integration scenarios', () => { - it('should support dynamic loading from npm with lazy activation', () => { - const loadRequest = DynamicLoadRequestSchema.parse({ - pluginId: 'com.acme.crm', - source: { - type: 'npm', - location: '@acme/crm-plugin', - version: '^3.0.0', - }, - activationEvents: [ - { type: 'onObject', pattern: 'account' }, - { type: 'onObject', pattern: 'contact' }, - { type: 'onRoute', pattern: '/api/v1/crm/*' }, - ], - sandbox: true, - }); - expect(loadRequest.activationEvents).toHaveLength(3); - expect(loadRequest.sandbox).toBe(true); - }); - - it('should support graceful unload with dependency awareness', () => { - const unloadRequest = DynamicUnloadRequestSchema.parse({ - pluginId: 'com.acme.analytics', - strategy: 'drain', - timeout: 60000, - dependentAction: 'cascade', - cleanupCache: true, - }); - expect(unloadRequest.strategy).toBe('drain'); - expect(unloadRequest.dependentAction).toBe('cascade'); - }); - - it('should support complete dynamic loading subsystem config', () => { - const config = DynamicLoadingConfigSchema.parse({ - enabled: true, - maxDynamicPlugins: 25, - discovery: { - enabled: true, - sources: [ - { - type: 'registry', - endpoint: 'https://plugins.objectstack.dev/api/v1', - pollInterval: 300000, - filter: { - minTrustLevel: 'trusted', - }, - }, - ], - autoLoad: false, - requireApproval: true, - }, - defaultSandbox: true, - allowedSources: ['npm', 'registry', 'local'], - requireIntegrity: true, - operationTimeout: 90000, - }); - expect(config.enabled).toBe(true); - expect(config.discovery?.sources).toHaveLength(1); - expect(config.discovery?.requireApproval).toBe(true); - expect(config.defaultSandbox).toBe(true); - }); - }); }); diff --git a/packages/spec/src/kernel/plugin-runtime.zod.ts b/packages/spec/src/kernel/plugin-runtime.zod.ts index 3b0a3a170f..73008270a1 100644 --- a/packages/spec/src/kernel/plugin-runtime.zod.ts +++ b/packages/spec/src/kernel/plugin-runtime.zod.ts @@ -224,137 +224,32 @@ export const DynamicPluginResultSchema = lazySchema(() => z.object({ }).describe('Result of a dynamic plugin operation')); /** - * Plugin Discovery Source - * Defines where to discover available plugins at runtime + * 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 const PluginDiscoverySourceSchema = lazySchema(() => z.object({ - /** - * Discovery source type - */ - type: z.enum([ - 'registry', // ObjectStack plugin registry API - 'npm', // npm registry search - 'directory', // Local filesystem directory scan - 'url', // Remote manifest URL - ]).describe('Discovery source type'), - - /** - * Source endpoint or path - */ - endpoint: z.string().describe('Registry URL, directory path, or manifest URL'), - - /** - * Polling interval in milliseconds (0 = manual only) - */ - pollInterval: z.number().int().min(0).default(0) - .describe('How often to re-scan for new plugins (0 = manual)'), - - /** - * Filter criteria for discovered plugins - */ - filter: z.object({ - /** - * Only discover plugins matching these tags - */ - tags: z.array(z.string()).optional(), - - /** - * Only discover plugins from these vendors - */ - vendors: z.array(z.string()).optional(), - - /** - * Minimum trust level - */ - minTrustLevel: z.enum(['verified', 'trusted', 'community', 'untrusted']).optional(), - }).optional(), -}).describe('Source for runtime plugin discovery')); - -/** - * Plugin Discovery Configuration - * Controls how the kernel discovers available plugins at runtime - */ -export const PluginDiscoveryConfigSchema = lazySchema(() => z.object({ - /** - * Enable runtime plugin discovery - */ - enabled: z.boolean().default(false), - - /** - * Discovery sources - */ - sources: z.array(PluginDiscoverySourceSchema).default([]), - - /** - * Auto-load discovered plugins matching criteria - */ - autoLoad: z.boolean().default(false) - .describe('Automatically load newly discovered plugins'), - - /** - * Require approval before loading discovered plugins - */ - requireApproval: z.boolean().default(true) - .describe('Require admin approval before loading discovered plugins'), -}).describe('Runtime plugin discovery configuration')); - -/** - * Dynamic Loading Configuration - * Top-level configuration for the dynamic plugin loading subsystem - */ -export const DynamicLoadingConfigSchema = lazySchema(() => z.object({ - /** - * Enable dynamic loading/unloading at runtime - */ - enabled: z.boolean().default(false) - .describe('Enable runtime load/unload of plugins'), - - /** - * Maximum number of dynamically loaded plugins - */ - maxDynamicPlugins: z.number().int().min(1).default(50) - .describe('Upper limit on runtime-loaded plugins'), - - /** - * Plugin discovery configuration - */ - discovery: PluginDiscoveryConfigSchema.optional(), - - /** - * Default sandbox policy for dynamically loaded plugins - */ - defaultSandbox: z.boolean().default(true) - .describe('Sandbox dynamically loaded plugins by default'), - - /** - * Allowed plugin sources — three-state, the same contract as - * `object.apiMethods` (#3391/#3543): `undefined` = no source policy declared, - * `[]` = deny-all, a subset = exactly those source types. - * - * The empty ARRAY is closed; only ABSENCE is open. The previous wording - * collapsed the two, making this a vacuous allow-list — one where the value an - * author reaches by mistake is also the widest grant. That is the shape #3896 - * turned into an over-share, and this is a supply-chain gate. - * - * Nothing enforces this field yet (hence the marker), which is precisely why - * the wording mattered: an unimplemented property's description is the - * specification whoever implements it builds to. - */ - allowedSources: z.array(z.enum(['npm', 'local', 'url', 'registry', 'git'])).optional() - .describe('[EXPERIMENTAL — not enforced] Restrict which plugin source types are permitted: undefined = any source, [] = deny-all, a subset = exactly those types'), - - /** - * Require integrity verification for remote plugins - */ - requireIntegrity: z.boolean().default(true) - .describe('Require integrity hash verification for remote sources'), - - /** - * Global timeout for dynamic operations in milliseconds - */ - operationTimeout: z.number().int().min(1000).default(60000) - .describe('Default timeout for load/unload operations in ms'), -}).describe('Dynamic plugin loading subsystem configuration')); // Export types export type DynamicPluginOperation = z.infer; @@ -363,11 +258,7 @@ export type ActivationEvent = z.infer; export type DynamicLoadRequest = z.infer; export type DynamicUnloadRequest = z.infer; export type DynamicPluginResult = z.infer; -export type PluginDiscoverySource = z.infer; -export type PluginDiscoveryConfig = z.infer; -export type DynamicLoadingConfig = z.infer; // Export input types for schemas with defaults export type DynamicLoadRequestInput = z.input; export type DynamicUnloadRequestInput = z.input; -export type DynamicLoadingConfigInput = z.input; From 133a2c59137ac49655a99e7109e2a595e5911314 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 12:35:29 +0000 Subject: [PATCH 2/3] chore(spec): regenerate the api-surface snapshot for the removed plugin-loading exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The third guard on a deliberate removal, after json-schema.manifest.json and authorable-surface.json: check:api-surface diffs the public export list and correctly reported 7 exports gone from ./kernel. Same exemption rationale as the other two baselines — the removal is the point of the PR, and the major changeset is already in place. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QuViRSR1j6GJjf9qGbnqFX --- packages/spec/api-surface.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index c8745503b0..7b84531b82 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -3026,8 +3026,6 @@ "UpdateManyDataRequestSchema (const)", "UpdateManyDataResponse (type)", "UpdateManyDataResponseSchema (const)", - "UpdateManyRecord (type)", - "UpdateManyRecordSchema (const)", "UpdateManyRequest (type)", "UpdateManyRequestSchema (const)", "UpdateNotificationPreferencesRequest (type)", @@ -3371,9 +3369,6 @@ "ReportNavItem (type)", "ReportNavItemSchema (const)", "ReportSchema (const)", - "ReportSort (type)", - "ReportSortInput (type)", - "ReportSortSchema (const)", "ReportType (const)", "ResolvedActionParam (interface)", "ResponsiveConfig (type)", @@ -3487,7 +3482,6 @@ "normalizeFilterOperator (function)", "pageForm (const)", "reportForm (const)", - "reportSelectionOrder (function)", "validateActionParams (function)", "viewForm (const)" ], From bf2fc89a3f019967f336cf7e12d67338ceb0f012 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 12:44:32 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix(spec):=20regenerate=20api-surface=20fro?= =?UTF-8?q?m=20a=20FRESH=20build=20=E2=80=94=20the=20previous=20snapshot?= =?UTF-8?q?=20came=20from=20stale=20dist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build-api-surface.ts reads the built dist (its header says to run it after a build). The previous regeneration ran against a dist compiled before this branch's removal — so the snapshot KEPT the 7 removed plugin-loading exports and DELETED six exports that had landed on main since (UpdateManyRecord*, ReportSort*, reportSelectionOrder). CI read that as 7 breaking + 6 added. Rebuilt, regenerated: the diff is now exactly the intended shape — the seven plugin-loading exports gone, the six main-side exports restored. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QuViRSR1j6GJjf9qGbnqFX --- packages/spec/api-surface.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index 7b84531b82..731171e585 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -1402,9 +1402,6 @@ "DynamicLoadRequest (type)", "DynamicLoadRequestInput (type)", "DynamicLoadRequestSchema (const)", - "DynamicLoadingConfig (type)", - "DynamicLoadingConfigInput (type)", - "DynamicLoadingConfigSchema (const)", "DynamicPluginOperation (type)", "DynamicPluginOperationSchema (const)", "DynamicPluginResult (type)", @@ -1630,10 +1627,6 @@ "PluginDependencyResolutionResultSchema (const)", "PluginDependencyResolutionSchema (const)", "PluginDependencySchema (const)", - "PluginDiscoveryConfig (type)", - "PluginDiscoveryConfigSchema (const)", - "PluginDiscoverySource (type)", - "PluginDiscoverySourceSchema (const)", "PluginDynamicImport (type)", "PluginDynamicImportSchema (const)", "PluginEngines (type)", @@ -3026,6 +3019,8 @@ "UpdateManyDataRequestSchema (const)", "UpdateManyDataResponse (type)", "UpdateManyDataResponseSchema (const)", + "UpdateManyRecord (type)", + "UpdateManyRecordSchema (const)", "UpdateManyRequest (type)", "UpdateManyRequestSchema (const)", "UpdateNotificationPreferencesRequest (type)", @@ -3369,6 +3364,9 @@ "ReportNavItem (type)", "ReportNavItemSchema (const)", "ReportSchema (const)", + "ReportSort (type)", + "ReportSortInput (type)", + "ReportSortSchema (const)", "ReportType (const)", "ResolvedActionParam (interface)", "ResponsiveConfig (type)", @@ -3482,6 +3480,7 @@ "normalizeFilterOperator (function)", "pageForm (const)", "reportForm (const)", + "reportSelectionOrder (function)", "validateActionParams (function)", "viewForm (const)" ],