From e239aa5fe4d807724da894ee17b639cf57fd8a3a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 08:16:40 +0000 Subject: [PATCH] =?UTF-8?q?feat(spec):=20add=20PLATFORM=5FPLUGIN=5FWIRED?= =?UTF-8?q?=5FRUNTIMES=20=E2=80=94=20provenance=20roster=20for=20plugins[]?= =?UTF-8?q?-wired=20out-of-repo=20runtimes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The token-keyed PLATFORM_CAPABILITY_PROVIDERS map structurally cannot hold @objectstack/organizations: it backs no requires token (serve loads it off the resolved tenancy posture, ADR-0105). Add a sibling, package-name-keyed provenance roster covering the plugins[]-wired out-of-repo population of two (organizations + security-enterprise), edition typed to exclude 'open' by construction. Provenance only: no new capability token, no widened key space, no posture-to-token resolution semantics. Drift pins in serve-capability-vocabulary.test.ts hold the two rosters to one edition wherever they name the same package, and derive enterprise-row membership so the next enterprise token is covered on arrival. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01T9cDbY2NBiVJWYx3BpWfH2 --- .changeset/plugin-wired-runtime-roster.md | 5 ++ .../test/serve-capability-vocabulary.test.ts | 59 ++++++++++++- packages/spec/api-surface/kernel.json | 2 + packages/spec/api-surface/root.json | 2 + packages/spec/export-origins/kernel.json | 2 + packages/spec/export-origins/root.json | 2 + packages/spec/src/index.ts | 5 ++ .../src/kernel/platform-capabilities.test.ts | 48 +++++++++++ .../spec/src/kernel/platform-capabilities.ts | 82 +++++++++++++++++++ 9 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 .changeset/plugin-wired-runtime-roster.md diff --git a/.changeset/plugin-wired-runtime-roster.md b/.changeset/plugin-wired-runtime-roster.md new file mode 100644 index 0000000000..5b95d4f077 --- /dev/null +++ b/.changeset/plugin-wired-runtime-roster.md @@ -0,0 +1,5 @@ +--- +'@objectstack/spec': minor +--- + +Add `PLATFORM_PLUGIN_WIRED_RUNTIMES` (and its row type `PlatformPluginWiredRuntime`) to the kernel platform-capability module: a companion provenance roster, keyed by npm package name, for the out-of-repo runtimes that reach the kernel through app `plugins[]` wiring rather than through a `requires` capability token — today `@objectstack/organizations` (loaded by `serve` off the resolved tenancy posture) and `@objectstack/security-enterprise` (which also backs the `hierarchy-security` token). The token-keyed `PLATFORM_CAPABILITY_PROVIDERS` map structurally cannot describe a package that backs no token; this roster makes "is this out-of-repo package real, and where does it ship from?" machine-readable for that population. Provenance only — it adds no capability token, changes no `requires` resolution, and encodes no posture-to-token semantics; drift tests pin the two rosters to agree wherever they name the same package. diff --git a/packages/cli/test/serve-capability-vocabulary.test.ts b/packages/cli/test/serve-capability-vocabulary.test.ts index 57aa2b5006..b9a35b03d6 100644 --- a/packages/cli/test/serve-capability-vocabulary.test.ts +++ b/packages/cli/test/serve-capability-vocabulary.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { PLATFORM_CAPABILITY_TOKENS, PLATFORM_CAPABILITY_PROVIDERS } from '@objectstack/spec/kernel'; +import { + PLATFORM_CAPABILITY_TOKENS, + PLATFORM_CAPABILITY_PROVIDERS, + PLATFORM_PLUGIN_WIRED_RUNTIMES, +} from '@objectstack/spec/kernel'; import Serve from '../src/commands/serve.js'; // framework#3265 — drift guard: the serve path's provider registries must stay @@ -74,3 +78,56 @@ describe('PLATFORM_CAPABILITY_PROVIDERS vs vocabulary + serve resolver (#3366)', expect(PLATFORM_CAPABILITY_PROVIDERS['ai-studio'].edition).toBe('cloud'); }); }); + +// #11263 — the companion roster for `plugins[]`-wired out-of-repo runtimes. +// The token-keyed map above structurally cannot hold `@objectstack/organizations` +// (it backs no `requires` token; `serve` loads it off the resolved tenancy +// posture), so its provenance row lives in PLATFORM_PLUGIN_WIRED_RUNTIMES. +// These pins keep the two rosters from diverging on the one fact they can both +// state, and keep the new one inside its own membership rule. +describe('PLATFORM_PLUGIN_WIRED_RUNTIMES vs providers + serve resolver (#11263)', () => { + it('declares @objectstack/organizations — the runtime serve loads off tenancy posture — as enterprise', () => { + const row = PLATFORM_PLUGIN_WIRED_RUNTIMES['@objectstack/organizations']; + expect(row, 'the package serve prints an install remedy for must have a provenance row').toBeTruthy(); + expect(row.edition).toBe('enterprise'); + }); + + it('every enterprise-edition provider package has a roster row — enterprise means plugins[]-wired, by definition', () => { + // CapabilityEdition's own definition: `enterprise` = "a separately-licensed + // enterprise package the app installs and wires in via `plugins[]`". So an + // enterprise provider row's package IS a plugins[]-wired out-of-repo + // runtime and must be declared in the companion roster too — this is what + // makes @objectstack/security-enterprise's double listing checked instead + // of divergent, and covers the next enterprise token on arrival. + const enterprisePackages = Object.entries(PLATFORM_CAPABILITY_PROVIDERS) + .filter(([, p]) => p.edition === 'enterprise' && p.package !== null) + .map(([token, p]) => [token, p.package as string] as const); + // Non-vacuity: hierarchy-security → @objectstack/security-enterprise is the + // standing member; an empty population would pass over nothing. + expect(enterprisePackages.length).toBeGreaterThan(0); + for (const [token, pkg] of enterprisePackages) { + const row = PLATFORM_PLUGIN_WIRED_RUNTIMES[pkg]; + expect(row, `enterprise provider '${token}' names '${pkg}', which has no roster row`).toBeTruthy(); + expect(row.edition, `edition drift for '${pkg}' between the two rosters`).toBe('enterprise'); + } + }); + + it('when both rosters name one package, they agree on its edition', () => { + // The generalized no-divergence pin, both editions covered: the only fact + // the two rosters can state twice must be stated identically. + for (const [pkg, row] of Object.entries(PLATFORM_PLUGIN_WIRED_RUNTIMES)) { + for (const [token, p] of Object.entries(PLATFORM_CAPABILITY_PROVIDERS)) { + if (p.package === pkg) { + expect(p.edition, `'${token}' and the roster disagree on '${pkg}'`).toBe(row.edition); + } + } + } + }); + + it('no roster package appears in serve CAPABILITY_PROVIDERS — plugins[]-wired is not requires-resolved by serve', () => { + const servePackages = Object.values(Serve.CAPABILITY_PROVIDERS).map((s) => s.pkg); + for (const pkg of Object.keys(PLATFORM_PLUGIN_WIRED_RUNTIMES)) { + expect(servePackages, `'${pkg}' is loaded by serve's open-edition resolver — it belongs in the token-keyed map`).not.toContain(pkg); + } + }); +}); diff --git a/packages/spec/api-surface/kernel.json b/packages/spec/api-surface/kernel.json index d4c34e869a..9a10cd2309 100644 --- a/packages/spec/api-surface/kernel.json +++ b/packages/spec/api-surface/kernel.json @@ -255,6 +255,7 @@ "PLATFORM_ALWAYS_ON_CAPABILITIES (const)", "PLATFORM_CAPABILITY_PROVIDERS (const)", "PLATFORM_CAPABILITY_TOKENS (const)", + "PLATFORM_PLUGIN_WIRED_RUNTIMES (const)", "PROTOCOL_MAJOR (const)", "PROTOCOL_VERSION (const)", "PUBLIC_AUTH_CONFIG_NON_FLAG_KEYS (const)", @@ -276,6 +277,7 @@ "PermissionScope (type)", "PermissionScopeSchema (const)", "PlatformCapabilityProvider (interface)", + "PlatformPluginWiredRuntime (interface)", "PluginCapability (type)", "PluginCapabilityManifest (type)", "PluginCapabilityManifestParsed (type)", diff --git a/packages/spec/api-surface/root.json b/packages/spec/api-surface/root.json index 3d0bbf5610..2380440211 100644 --- a/packages/spec/api-surface/root.json +++ b/packages/spec/api-surface/root.json @@ -93,7 +93,9 @@ "PLATFORM_ALWAYS_ON_CAPABILITIES (const)", "PLATFORM_CAPABILITY_PROVIDERS (const)", "PLATFORM_CAPABILITY_TOKENS (const)", + "PLATFORM_PLUGIN_WIRED_RUNTIMES (const)", "PlatformCapabilityProvider (interface)", + "PlatformPluginWiredRuntime (interface)", "PluginContext (type)", "Predicate (type)", "PredicateInput (type)", diff --git a/packages/spec/export-origins/kernel.json b/packages/spec/export-origins/kernel.json index 0a694296bd..e9e5675d12 100644 --- a/packages/spec/export-origins/kernel.json +++ b/packages/spec/export-origins/kernel.json @@ -255,6 +255,7 @@ "PLATFORM_ALWAYS_ON_CAPABILITIES": "src/kernel/platform-capabilities.ts#PLATFORM_ALWAYS_ON_CAPABILITIES (const)", "PLATFORM_CAPABILITY_PROVIDERS": "src/kernel/platform-capabilities.ts#PLATFORM_CAPABILITY_PROVIDERS (const)", "PLATFORM_CAPABILITY_TOKENS": "src/kernel/platform-capabilities.ts#PLATFORM_CAPABILITY_TOKENS (const)", + "PLATFORM_PLUGIN_WIRED_RUNTIMES": "src/kernel/platform-capabilities.ts#PLATFORM_PLUGIN_WIRED_RUNTIMES (const)", "PROTOCOL_MAJOR": "src/kernel/protocol-version.ts#PROTOCOL_MAJOR (const)", "PROTOCOL_VERSION": "src/kernel/protocol-version.ts#PROTOCOL_VERSION (const)", "PUBLIC_AUTH_CONFIG_NON_FLAG_KEYS": "src/kernel/public-auth-features.ts#PUBLIC_AUTH_CONFIG_NON_FLAG_KEYS (const)", @@ -276,6 +277,7 @@ "PermissionScope": "src/kernel/plugin-security-advanced.zod.ts#PermissionScope (type)", "PermissionScopeSchema": "src/kernel/plugin-security-advanced.zod.ts#PermissionScopeSchema (const)", "PlatformCapabilityProvider": "src/kernel/platform-capabilities.ts#PlatformCapabilityProvider (interface)", + "PlatformPluginWiredRuntime": "src/kernel/platform-capabilities.ts#PlatformPluginWiredRuntime (interface)", "PluginCapability": "src/kernel/plugin-capability.zod.ts#PluginCapability (type)", "PluginCapabilityManifest": "src/kernel/plugin-capability.zod.ts#PluginCapabilityManifest (type)", "PluginCapabilityManifestParsed": "src/kernel/plugin-capability.zod.ts#PluginCapabilityManifestParsed (type)", diff --git a/packages/spec/export-origins/root.json b/packages/spec/export-origins/root.json index 6699a2fd4a..310f4918e8 100644 --- a/packages/spec/export-origins/root.json +++ b/packages/spec/export-origins/root.json @@ -93,7 +93,9 @@ "PLATFORM_ALWAYS_ON_CAPABILITIES": "src/kernel/platform-capabilities.ts#PLATFORM_ALWAYS_ON_CAPABILITIES (const)", "PLATFORM_CAPABILITY_PROVIDERS": "src/kernel/platform-capabilities.ts#PLATFORM_CAPABILITY_PROVIDERS (const)", "PLATFORM_CAPABILITY_TOKENS": "src/kernel/platform-capabilities.ts#PLATFORM_CAPABILITY_TOKENS (const)", + "PLATFORM_PLUGIN_WIRED_RUNTIMES": "src/kernel/platform-capabilities.ts#PLATFORM_PLUGIN_WIRED_RUNTIMES (const)", "PlatformCapabilityProvider": "src/kernel/platform-capabilities.ts#PlatformCapabilityProvider (interface)", + "PlatformPluginWiredRuntime": "src/kernel/platform-capabilities.ts#PlatformPluginWiredRuntime (interface)", "PluginContext": "src/kernel/plugin.zod.ts#PluginContext (type)", "Predicate": "src/shared/expression.zod.ts#Predicate (type)", "PredicateInput": "src/shared/expression.zod.ts#PredicateInput (type)", diff --git a/packages/spec/src/index.ts b/packages/spec/src/index.ts index d7e1044a50..001c445b89 100644 --- a/packages/spec/src/index.ts +++ b/packages/spec/src/index.ts @@ -199,12 +199,17 @@ export { PLATFORM_CAPABILITY_TOKENS, isKnownPlatformCapability, PLATFORM_CAPABILITY_PROVIDERS, + // The `plugins[]`-wired out-of-repo runtimes the token-keyed map structurally + // cannot describe (no `requires` token to key a row by) — provenance only, + // never resolution (#10921, #11263). + PLATFORM_PLUGIN_WIRED_RUNTIMES, // The foundational slate every server-side runtime mounts (cloud#925, #3786) — // one declaration for `objectstack serve` and cloud's per-tenant runtime alike. PLATFORM_ALWAYS_ON_CAPABILITIES, classifyRequiredCapability, type CapabilityEdition, type PlatformCapabilityProvider, + type PlatformPluginWiredRuntime, type CapabilityProviderStatus, type CapabilityClassification, } from './kernel/platform-capabilities'; diff --git a/packages/spec/src/kernel/platform-capabilities.test.ts b/packages/spec/src/kernel/platform-capabilities.test.ts index 6051747626..7118bace3e 100644 --- a/packages/spec/src/kernel/platform-capabilities.test.ts +++ b/packages/spec/src/kernel/platform-capabilities.test.ts @@ -3,6 +3,7 @@ import { PLATFORM_CAPABILITY_TOKENS, isKnownPlatformCapability, PLATFORM_CAPABILITY_PROVIDERS, + PLATFORM_PLUGIN_WIRED_RUNTIMES, PLATFORM_ALWAYS_ON_CAPABILITIES, classifyRequiredCapability, } from './platform-capabilities'; @@ -70,6 +71,53 @@ describe('PLATFORM_CAPABILITY_PROVIDERS', () => { }); }); +// #11263 — the companion roster for `plugins[]`-wired out-of-repo runtimes, +// which the token-keyed provider map structurally cannot describe (no +// `requires` token to key a row by). Shape invariants live here; the +// cross-registry drift pins (edition agreement with the token-keyed map, the +// serve-resolver exclusions) live in the CLI's +// `serve-capability-vocabulary.test.ts` beside the map's own 1:1 pins. +describe('PLATFORM_PLUGIN_WIRED_RUNTIMES (#11263)', () => { + it('is frozen and non-empty', () => { + expect(Object.isFrozen(PLATFORM_PLUGIN_WIRED_RUNTIMES)).toBe(true); + expect(Object.keys(PLATFORM_PLUGIN_WIRED_RUNTIMES).length).toBeGreaterThan(0); + }); + + it('every key is an @objectstack/ npm package name — the key IS the package', () => { + for (const pkg of Object.keys(PLATFORM_PLUGIN_WIRED_RUNTIMES)) { + expect(pkg).toMatch(/^@objectstack\/[a-z0-9-]+$/); + } + }); + + it('every row carries a non-empty provenance note — recording it is the roster’s whole job', () => { + for (const [pkg, row] of Object.entries(PLATFORM_PLUGIN_WIRED_RUNTIMES)) { + expect(row.note.trim().length, `empty note for '${pkg}'`).toBeGreaterThan(0); + // The type already excludes 'open'; assert the runtime value too so a + // cast or a JS caller cannot smuggle one in. + expect(['enterprise', 'cloud'], `bad edition for '${pkg}'`).toContain(row.edition); + } + }); + + it('keys are DISJOINT from the capability vocabulary — package names, never tokens', () => { + // The single-list rule's load-bearing half: this roster must never grow a + // second way to spell a `requires` token. A key that is also a vocabulary + // token would be exactly the divergent second description the provider + // map's header warns against. + for (const pkg of Object.keys(PLATFORM_PLUGIN_WIRED_RUNTIMES)) { + expect(PLATFORM_CAPABILITY_TOKENS).not.toContain(pkg); + } + }); + + it('no roster package is an open-edition provider package — plugins[]-wired means not requires-resolved as open', () => { + const openPackages = Object.values(PLATFORM_CAPABILITY_PROVIDERS) + .filter((p) => p.edition === 'open') + .map((p) => p.package); + for (const pkg of Object.keys(PLATFORM_PLUGIN_WIRED_RUNTIMES)) { + expect(openPackages).not.toContain(pkg); + } + }); +}); + describe('classifyRequiredCapability (#3366)', () => { const allInstalled = () => true; const noneInstalled = () => false; diff --git a/packages/spec/src/kernel/platform-capabilities.ts b/packages/spec/src/kernel/platform-capabilities.ts index dee97ad3c2..48d7f1b2ae 100644 --- a/packages/spec/src/kernel/platform-capabilities.ts +++ b/packages/spec/src/kernel/platform-capabilities.ts @@ -117,6 +117,11 @@ export interface PlatformCapabilityProvider { * A drift test (`serve-capability-vocabulary.test.ts`) asserts this map and the * vocabulary stay in 1:1 sync, and that every `open`-edition entry agrees with * the package the serve resolver actually loads — so the two can't diverge. + * + * One population is structurally OUTSIDE this map's key space: an out-of-repo + * runtime wired in via `plugins[]` that backs no `requires` token has no token + * to key a row by. {@link PLATFORM_PLUGIN_WIRED_RUNTIMES} is the sibling roster + * that records those — same provenance shape, keyed by package name. */ export const PLATFORM_CAPABILITY_PROVIDERS: Readonly> = Object.freeze({ @@ -168,6 +173,83 @@ export const PLATFORM_CAPABILITY_PROVIDERS: Readonly; + /** + * Short human note on where the runtime ships from and how it is loaded. + * Required, not optional: recording that provenance is this roster's entire + * job — a row without it answers nothing. + */ + readonly note: string; +} + +/** + * The `plugins[]`-wired out-of-repo runtimes, keyed by npm package name — the + * companion roster to {@link PLATFORM_CAPABILITY_PROVIDERS} for the packages + * whose loading is NOT keyed by a `requires` token (#10921, #11263). + * + * Why it exists: this tree names closed-source `@objectstack/` packages it does + * not build — `serve` prints an install instruction for one of them at + * operators — and until this roster, "is that a real out-of-repo package or a + * fabricated name?" was answerable only by grep-and-judgement. A fabricated + * package name (`@objectstack/framework`) sat next to a real one + * (`@objectstack/organizations`) in published docs for months looking identical + * (#10921). A row here is the machine-readable declaration that the package is + * real and where it ships from. + * + * What this roster deliberately is NOT: a resolution registry. It does not make + * these packages `requires`-resolvable, adds nothing to + * {@link PLATFORM_CAPABILITY_TOKENS}, and does not relate tenancy posture to + * the capability vocabulary. Each runtime's load condition lives in the runtime + * that loads it (`serve` loads `@objectstack/organizations` off the resolved + * tenancy posture, ADR-0105; an app wires `@objectstack/security-enterprise` + * into its own `plugins[]`) — the rows record that fact as prose provenance, + * they do not encode it as a lookup. + * + * On the single-list rule (see {@link PLATFORM_ALWAYS_ON_CAPABILITIES} on why a + * second description nobody checks is a defect): this roster's key space is + * DISJOINT from the token-keyed map — package names, not tokens — and the one + * fact the two can state twice (the edition of a package that also backs a + * token, today `@objectstack/security-enterprise` behind `hierarchy-security`) + * is drift-tested to agree, in `serve-capability-vocabulary.test.ts` alongside + * the map's own 1:1 pins. The same test derives membership the other way too: + * every `enterprise`-edition provider row names a `plugins[]`-wired package (by + * {@link CapabilityEdition}'s own definition), so each must have a row here. + * + * Growing it: a new out-of-repo `plugins[]`-wired runtime adds its row HERE in + * the PR that first names the package at operators or in published docs. + */ +export const PLATFORM_PLUGIN_WIRED_RUNTIMES: Readonly> = + Object.freeze({ + '@objectstack/organizations': { + edition: 'enterprise', + note: + 'closed-source multi-org runtime (ADR-0105); `serve` loads it from the host app ' + + 'when the resolved tenancy posture is `group`/`isolated` — no `requires` token; ' + + 'not on the public npm registry, distributed with an enterprise/cloud subscription', + }, + '@objectstack/security-enterprise': { + edition: 'enterprise', + note: + 'closed-source enterprise security runtime; the app wires it in via `plugins[]`. ' + + 'Also backs the `hierarchy-security` token above (ADR-0057) — the drift test holds ' + + 'both rows to one edition', + }, + }); + /** * The foundational capability slate: what every server-side runtime is expected * to mount whether or not an app names it in `requires`.