Skip to content

Commit 177ebdc

Browse files
os-steveclaude
andauthored
feat(spec): add PLATFORM_PLUGIN_WIRED_RUNTIMES — provenance roster for plugins[]-wired out-of-repo runtimes (#11613)
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. Claude-Session: https://claude.ai/code/session_01T9cDbY2NBiVJWYx3BpWfH2 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5de2b0f commit 177ebdc

9 files changed

Lines changed: 206 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
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.

packages/cli/test/serve-capability-vocabulary.test.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { describe, expect, it } from 'vitest';
2-
import { PLATFORM_CAPABILITY_TOKENS, PLATFORM_CAPABILITY_PROVIDERS } from '@objectstack/spec/kernel';
2+
import {
3+
PLATFORM_CAPABILITY_TOKENS,
4+
PLATFORM_CAPABILITY_PROVIDERS,
5+
PLATFORM_PLUGIN_WIRED_RUNTIMES,
6+
} from '@objectstack/spec/kernel';
37
import Serve from '../src/commands/serve.js';
48

59
// 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)',
7478
expect(PLATFORM_CAPABILITY_PROVIDERS['ai-studio'].edition).toBe('cloud');
7579
});
7680
});
81+
82+
// #11263 — the companion roster for `plugins[]`-wired out-of-repo runtimes.
83+
// The token-keyed map above structurally cannot hold `@objectstack/organizations`
84+
// (it backs no `requires` token; `serve` loads it off the resolved tenancy
85+
// posture), so its provenance row lives in PLATFORM_PLUGIN_WIRED_RUNTIMES.
86+
// These pins keep the two rosters from diverging on the one fact they can both
87+
// state, and keep the new one inside its own membership rule.
88+
describe('PLATFORM_PLUGIN_WIRED_RUNTIMES vs providers + serve resolver (#11263)', () => {
89+
it('declares @objectstack/organizations — the runtime serve loads off tenancy posture — as enterprise', () => {
90+
const row = PLATFORM_PLUGIN_WIRED_RUNTIMES['@objectstack/organizations'];
91+
expect(row, 'the package serve prints an install remedy for must have a provenance row').toBeTruthy();
92+
expect(row.edition).toBe('enterprise');
93+
});
94+
95+
it('every enterprise-edition provider package has a roster row — enterprise means plugins[]-wired, by definition', () => {
96+
// CapabilityEdition's own definition: `enterprise` = "a separately-licensed
97+
// enterprise package the app installs and wires in via `plugins[]`". So an
98+
// enterprise provider row's package IS a plugins[]-wired out-of-repo
99+
// runtime and must be declared in the companion roster too — this is what
100+
// makes @objectstack/security-enterprise's double listing checked instead
101+
// of divergent, and covers the next enterprise token on arrival.
102+
const enterprisePackages = Object.entries(PLATFORM_CAPABILITY_PROVIDERS)
103+
.filter(([, p]) => p.edition === 'enterprise' && p.package !== null)
104+
.map(([token, p]) => [token, p.package as string] as const);
105+
// Non-vacuity: hierarchy-security → @objectstack/security-enterprise is the
106+
// standing member; an empty population would pass over nothing.
107+
expect(enterprisePackages.length).toBeGreaterThan(0);
108+
for (const [token, pkg] of enterprisePackages) {
109+
const row = PLATFORM_PLUGIN_WIRED_RUNTIMES[pkg];
110+
expect(row, `enterprise provider '${token}' names '${pkg}', which has no roster row`).toBeTruthy();
111+
expect(row.edition, `edition drift for '${pkg}' between the two rosters`).toBe('enterprise');
112+
}
113+
});
114+
115+
it('when both rosters name one package, they agree on its edition', () => {
116+
// The generalized no-divergence pin, both editions covered: the only fact
117+
// the two rosters can state twice must be stated identically.
118+
for (const [pkg, row] of Object.entries(PLATFORM_PLUGIN_WIRED_RUNTIMES)) {
119+
for (const [token, p] of Object.entries(PLATFORM_CAPABILITY_PROVIDERS)) {
120+
if (p.package === pkg) {
121+
expect(p.edition, `'${token}' and the roster disagree on '${pkg}'`).toBe(row.edition);
122+
}
123+
}
124+
}
125+
});
126+
127+
it('no roster package appears in serve CAPABILITY_PROVIDERS — plugins[]-wired is not requires-resolved by serve', () => {
128+
const servePackages = Object.values(Serve.CAPABILITY_PROVIDERS).map((s) => s.pkg);
129+
for (const pkg of Object.keys(PLATFORM_PLUGIN_WIRED_RUNTIMES)) {
130+
expect(servePackages, `'${pkg}' is loaded by serve's open-edition resolver — it belongs in the token-keyed map`).not.toContain(pkg);
131+
}
132+
});
133+
});

packages/spec/api-surface/kernel.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@
255255
"PLATFORM_ALWAYS_ON_CAPABILITIES (const)",
256256
"PLATFORM_CAPABILITY_PROVIDERS (const)",
257257
"PLATFORM_CAPABILITY_TOKENS (const)",
258+
"PLATFORM_PLUGIN_WIRED_RUNTIMES (const)",
258259
"PROTOCOL_MAJOR (const)",
259260
"PROTOCOL_VERSION (const)",
260261
"PUBLIC_AUTH_CONFIG_NON_FLAG_KEYS (const)",
@@ -276,6 +277,7 @@
276277
"PermissionScope (type)",
277278
"PermissionScopeSchema (const)",
278279
"PlatformCapabilityProvider (interface)",
280+
"PlatformPluginWiredRuntime (interface)",
279281
"PluginCapability (type)",
280282
"PluginCapabilityManifest (type)",
281283
"PluginCapabilityManifestParsed (type)",

packages/spec/api-surface/root.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@
9393
"PLATFORM_ALWAYS_ON_CAPABILITIES (const)",
9494
"PLATFORM_CAPABILITY_PROVIDERS (const)",
9595
"PLATFORM_CAPABILITY_TOKENS (const)",
96+
"PLATFORM_PLUGIN_WIRED_RUNTIMES (const)",
9697
"PlatformCapabilityProvider (interface)",
98+
"PlatformPluginWiredRuntime (interface)",
9799
"PluginContext (type)",
98100
"Predicate (type)",
99101
"PredicateInput (type)",

packages/spec/export-origins/kernel.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@
255255
"PLATFORM_ALWAYS_ON_CAPABILITIES": "src/kernel/platform-capabilities.ts#PLATFORM_ALWAYS_ON_CAPABILITIES (const)",
256256
"PLATFORM_CAPABILITY_PROVIDERS": "src/kernel/platform-capabilities.ts#PLATFORM_CAPABILITY_PROVIDERS (const)",
257257
"PLATFORM_CAPABILITY_TOKENS": "src/kernel/platform-capabilities.ts#PLATFORM_CAPABILITY_TOKENS (const)",
258+
"PLATFORM_PLUGIN_WIRED_RUNTIMES": "src/kernel/platform-capabilities.ts#PLATFORM_PLUGIN_WIRED_RUNTIMES (const)",
258259
"PROTOCOL_MAJOR": "src/kernel/protocol-version.ts#PROTOCOL_MAJOR (const)",
259260
"PROTOCOL_VERSION": "src/kernel/protocol-version.ts#PROTOCOL_VERSION (const)",
260261
"PUBLIC_AUTH_CONFIG_NON_FLAG_KEYS": "src/kernel/public-auth-features.ts#PUBLIC_AUTH_CONFIG_NON_FLAG_KEYS (const)",
@@ -276,6 +277,7 @@
276277
"PermissionScope": "src/kernel/plugin-security-advanced.zod.ts#PermissionScope (type)",
277278
"PermissionScopeSchema": "src/kernel/plugin-security-advanced.zod.ts#PermissionScopeSchema (const)",
278279
"PlatformCapabilityProvider": "src/kernel/platform-capabilities.ts#PlatformCapabilityProvider (interface)",
280+
"PlatformPluginWiredRuntime": "src/kernel/platform-capabilities.ts#PlatformPluginWiredRuntime (interface)",
279281
"PluginCapability": "src/kernel/plugin-capability.zod.ts#PluginCapability (type)",
280282
"PluginCapabilityManifest": "src/kernel/plugin-capability.zod.ts#PluginCapabilityManifest (type)",
281283
"PluginCapabilityManifestParsed": "src/kernel/plugin-capability.zod.ts#PluginCapabilityManifestParsed (type)",

packages/spec/export-origins/root.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@
9393
"PLATFORM_ALWAYS_ON_CAPABILITIES": "src/kernel/platform-capabilities.ts#PLATFORM_ALWAYS_ON_CAPABILITIES (const)",
9494
"PLATFORM_CAPABILITY_PROVIDERS": "src/kernel/platform-capabilities.ts#PLATFORM_CAPABILITY_PROVIDERS (const)",
9595
"PLATFORM_CAPABILITY_TOKENS": "src/kernel/platform-capabilities.ts#PLATFORM_CAPABILITY_TOKENS (const)",
96+
"PLATFORM_PLUGIN_WIRED_RUNTIMES": "src/kernel/platform-capabilities.ts#PLATFORM_PLUGIN_WIRED_RUNTIMES (const)",
9697
"PlatformCapabilityProvider": "src/kernel/platform-capabilities.ts#PlatformCapabilityProvider (interface)",
98+
"PlatformPluginWiredRuntime": "src/kernel/platform-capabilities.ts#PlatformPluginWiredRuntime (interface)",
9799
"PluginContext": "src/kernel/plugin.zod.ts#PluginContext (type)",
98100
"Predicate": "src/shared/expression.zod.ts#Predicate (type)",
99101
"PredicateInput": "src/shared/expression.zod.ts#PredicateInput (type)",

packages/spec/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,12 +199,17 @@ export {
199199
PLATFORM_CAPABILITY_TOKENS,
200200
isKnownPlatformCapability,
201201
PLATFORM_CAPABILITY_PROVIDERS,
202+
// The `plugins[]`-wired out-of-repo runtimes the token-keyed map structurally
203+
// cannot describe (no `requires` token to key a row by) — provenance only,
204+
// never resolution (#10921, #11263).
205+
PLATFORM_PLUGIN_WIRED_RUNTIMES,
202206
// The foundational slate every server-side runtime mounts (cloud#925, #3786) —
203207
// one declaration for `objectstack serve` and cloud's per-tenant runtime alike.
204208
PLATFORM_ALWAYS_ON_CAPABILITIES,
205209
classifyRequiredCapability,
206210
type CapabilityEdition,
207211
type PlatformCapabilityProvider,
212+
type PlatformPluginWiredRuntime,
208213
type CapabilityProviderStatus,
209214
type CapabilityClassification,
210215
} from './kernel/platform-capabilities';

packages/spec/src/kernel/platform-capabilities.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
PLATFORM_CAPABILITY_TOKENS,
44
isKnownPlatformCapability,
55
PLATFORM_CAPABILITY_PROVIDERS,
6+
PLATFORM_PLUGIN_WIRED_RUNTIMES,
67
PLATFORM_ALWAYS_ON_CAPABILITIES,
78
classifyRequiredCapability,
89
} from './platform-capabilities';
@@ -70,6 +71,53 @@ describe('PLATFORM_CAPABILITY_PROVIDERS', () => {
7071
});
7172
});
7273

74+
// #11263 — the companion roster for `plugins[]`-wired out-of-repo runtimes,
75+
// which the token-keyed provider map structurally cannot describe (no
76+
// `requires` token to key a row by). Shape invariants live here; the
77+
// cross-registry drift pins (edition agreement with the token-keyed map, the
78+
// serve-resolver exclusions) live in the CLI's
79+
// `serve-capability-vocabulary.test.ts` beside the map's own 1:1 pins.
80+
describe('PLATFORM_PLUGIN_WIRED_RUNTIMES (#11263)', () => {
81+
it('is frozen and non-empty', () => {
82+
expect(Object.isFrozen(PLATFORM_PLUGIN_WIRED_RUNTIMES)).toBe(true);
83+
expect(Object.keys(PLATFORM_PLUGIN_WIRED_RUNTIMES).length).toBeGreaterThan(0);
84+
});
85+
86+
it('every key is an @objectstack/ npm package name — the key IS the package', () => {
87+
for (const pkg of Object.keys(PLATFORM_PLUGIN_WIRED_RUNTIMES)) {
88+
expect(pkg).toMatch(/^@objectstack\/[a-z0-9-]+$/);
89+
}
90+
});
91+
92+
it('every row carries a non-empty provenance note — recording it is the roster’s whole job', () => {
93+
for (const [pkg, row] of Object.entries(PLATFORM_PLUGIN_WIRED_RUNTIMES)) {
94+
expect(row.note.trim().length, `empty note for '${pkg}'`).toBeGreaterThan(0);
95+
// The type already excludes 'open'; assert the runtime value too so a
96+
// cast or a JS caller cannot smuggle one in.
97+
expect(['enterprise', 'cloud'], `bad edition for '${pkg}'`).toContain(row.edition);
98+
}
99+
});
100+
101+
it('keys are DISJOINT from the capability vocabulary — package names, never tokens', () => {
102+
// The single-list rule's load-bearing half: this roster must never grow a
103+
// second way to spell a `requires` token. A key that is also a vocabulary
104+
// token would be exactly the divergent second description the provider
105+
// map's header warns against.
106+
for (const pkg of Object.keys(PLATFORM_PLUGIN_WIRED_RUNTIMES)) {
107+
expect(PLATFORM_CAPABILITY_TOKENS).not.toContain(pkg);
108+
}
109+
});
110+
111+
it('no roster package is an open-edition provider package — plugins[]-wired means not requires-resolved as open', () => {
112+
const openPackages = Object.values(PLATFORM_CAPABILITY_PROVIDERS)
113+
.filter((p) => p.edition === 'open')
114+
.map((p) => p.package);
115+
for (const pkg of Object.keys(PLATFORM_PLUGIN_WIRED_RUNTIMES)) {
116+
expect(openPackages).not.toContain(pkg);
117+
}
118+
});
119+
});
120+
73121
describe('classifyRequiredCapability (#3366)', () => {
74122
const allInstalled = () => true;
75123
const noneInstalled = () => false;

packages/spec/src/kernel/platform-capabilities.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ export interface PlatformCapabilityProvider {
117117
* A drift test (`serve-capability-vocabulary.test.ts`) asserts this map and the
118118
* vocabulary stay in 1:1 sync, and that every `open`-edition entry agrees with
119119
* the package the serve resolver actually loads — so the two can't diverge.
120+
*
121+
* One population is structurally OUTSIDE this map's key space: an out-of-repo
122+
* runtime wired in via `plugins[]` that backs no `requires` token has no token
123+
* to key a row by. {@link PLATFORM_PLUGIN_WIRED_RUNTIMES} is the sibling roster
124+
* that records those — same provenance shape, keyed by package name.
120125
*/
121126
export const PLATFORM_CAPABILITY_PROVIDERS: Readonly<Record<string, PlatformCapabilityProvider>> =
122127
Object.freeze({
@@ -168,6 +173,83 @@ export const PLATFORM_CAPABILITY_PROVIDERS: Readonly<Record<string, PlatformCapa
168173
governance: { package: null, edition: 'cloud', note: 'cloud governance tier' },
169174
});
170175

176+
/**
177+
* Provenance row for one out-of-repo platform runtime that reaches the kernel
178+
* through app `plugins[]` wiring (host-app declaration) rather than through a
179+
* `requires` capability token. Same provenance facts as
180+
* {@link PlatformCapabilityProvider} minus `package` (here the package name is
181+
* the KEY, so it cannot be `null` and cannot drift from a duplicate field).
182+
*/
183+
export interface PlatformPluginWiredRuntime {
184+
/**
185+
* Which edition ships the runtime. Never `open` — an open-edition package is
186+
* part of the public framework distribution and is resolved through a
187+
* `requires` token, so it belongs in {@link PLATFORM_CAPABILITY_PROVIDERS},
188+
* not here. The exclusion is type-level so a mis-filed row cannot compile.
189+
*/
190+
readonly edition: Exclude<CapabilityEdition, 'open'>;
191+
/**
192+
* Short human note on where the runtime ships from and how it is loaded.
193+
* Required, not optional: recording that provenance is this roster's entire
194+
* job — a row without it answers nothing.
195+
*/
196+
readonly note: string;
197+
}
198+
199+
/**
200+
* The `plugins[]`-wired out-of-repo runtimes, keyed by npm package name — the
201+
* companion roster to {@link PLATFORM_CAPABILITY_PROVIDERS} for the packages
202+
* whose loading is NOT keyed by a `requires` token (#10921, #11263).
203+
*
204+
* Why it exists: this tree names closed-source `@objectstack/` packages it does
205+
* not build — `serve` prints an install instruction for one of them at
206+
* operators — and until this roster, "is that a real out-of-repo package or a
207+
* fabricated name?" was answerable only by grep-and-judgement. A fabricated
208+
* package name (`@objectstack/framework`) sat next to a real one
209+
* (`@objectstack/organizations`) in published docs for months looking identical
210+
* (#10921). A row here is the machine-readable declaration that the package is
211+
* real and where it ships from.
212+
*
213+
* What this roster deliberately is NOT: a resolution registry. It does not make
214+
* these packages `requires`-resolvable, adds nothing to
215+
* {@link PLATFORM_CAPABILITY_TOKENS}, and does not relate tenancy posture to
216+
* the capability vocabulary. Each runtime's load condition lives in the runtime
217+
* that loads it (`serve` loads `@objectstack/organizations` off the resolved
218+
* tenancy posture, ADR-0105; an app wires `@objectstack/security-enterprise`
219+
* into its own `plugins[]`) — the rows record that fact as prose provenance,
220+
* they do not encode it as a lookup.
221+
*
222+
* On the single-list rule (see {@link PLATFORM_ALWAYS_ON_CAPABILITIES} on why a
223+
* second description nobody checks is a defect): this roster's key space is
224+
* DISJOINT from the token-keyed map — package names, not tokens — and the one
225+
* fact the two can state twice (the edition of a package that also backs a
226+
* token, today `@objectstack/security-enterprise` behind `hierarchy-security`)
227+
* is drift-tested to agree, in `serve-capability-vocabulary.test.ts` alongside
228+
* the map's own 1:1 pins. The same test derives membership the other way too:
229+
* every `enterprise`-edition provider row names a `plugins[]`-wired package (by
230+
* {@link CapabilityEdition}'s own definition), so each must have a row here.
231+
*
232+
* Growing it: a new out-of-repo `plugins[]`-wired runtime adds its row HERE in
233+
* the PR that first names the package at operators or in published docs.
234+
*/
235+
export const PLATFORM_PLUGIN_WIRED_RUNTIMES: Readonly<Record<string, PlatformPluginWiredRuntime>> =
236+
Object.freeze({
237+
'@objectstack/organizations': {
238+
edition: 'enterprise',
239+
note:
240+
'closed-source multi-org runtime (ADR-0105); `serve` loads it from the host app ' +
241+
'when the resolved tenancy posture is `group`/`isolated` — no `requires` token; ' +
242+
'not on the public npm registry, distributed with an enterprise/cloud subscription',
243+
},
244+
'@objectstack/security-enterprise': {
245+
edition: 'enterprise',
246+
note:
247+
'closed-source enterprise security runtime; the app wires it in via `plugins[]`. ' +
248+
'Also backs the `hierarchy-security` token above (ADR-0057) — the drift test holds ' +
249+
'both rows to one edition',
250+
},
251+
});
252+
171253
/**
172254
* The foundational capability slate: what every server-side runtime is expected
173255
* to mount whether or not an app names it in `requires`.

0 commit comments

Comments
 (0)