Repository navigation
Unify optional-plugin loading: framework serve.ts and cloud objectos-runtime are parallel implementations with divergent capability tokens (ai-studio vs aiStudio) #3265
Description
Activity
Decision (maintainer-approved) + implementation started.
Direction: Phase 1 + small Phase-2 core — unify the token vocabulary with authoring-time validation, and share only the small drift-prone core; the full shared-loader extraction (option 1) is deliberately deferred as likely over-engineering (the two loaders' glue — tier gating / CLI presets vs per-kernel bundle options / enterprise gates — is genuinely different; the drift-prone core is small).
Canonical spelling: kebab-case
ai-studio(consistent withpinyin-search/hierarchy-security;requiresis a spec-owned contract field, so the spec's convention wins).aiStudio/aiSeatbecome deprecated aliases for one release.Implementation:
- framework#3281 (draft): spec-owned
PLATFORM_CAPABILITY_TOKENS+DEPRECATED_PLATFORM_CAPABILITY_ALIASES+canonicalizePlatformCapability;defineStackrewrites aliases at authoring time and warns on unknown tokens (warn-first → intended to become an error once the vocabulary proves complete);serve.tscanonicalizes raw artifact input, warns on declared-but-unknown tokens (closing the silent-typo hole), and its registries are statics with a drift test against the vocabulary; the missing-vs-crashed classifier moved to shared@objectstack/types isModuleNotFoundError. - cloud PR (in progress):
capability-loader.tskeys →ai-studio/ai-seatwith local alias canonicalization + deprecation warning (adopts the shared spec/types helpers at its next framework pin bump), kernel-factory AI-seat gate → canonical tokens,enterpriseCapabilityRequires()→['ai-studio', ...], configs/tests/docs migrated. Feature/entitlement ids (FEATURE_AI_STUDIO='aiStudio') and the runtime-config wire contract (features.aiStudio) are separate vocabularies — unchanged.
Remaining after both PRs: flip the unknown-token warn to a reject once the vocabulary is proven complete, and drop the aliases after one release.
Generated by Claude Code
- framework#3281 (draft): spec-owned
- added a commit that references this issue
on Jul 19, 2026 - added a commit that references this issue
on Jul 19, 2026 Done — closing as completed
Both findings are resolved and all work is merged. The framework CLI serve path and cloud's
objectos-runtimenow share one capability vocabulary and one implementation of the canonicalization + module-not-found logic, so the parallel-loader drift and theai-studio/aiStudiodivergence are structurally gone.Merged PRs:
- framework feat(spec,cli): one platform capability vocabulary — canonical kebab tokens, deprecated aliases, warn-first validation (#3265) #3281 — spec-owned
PLATFORM_CAPABILITY_TOKENS+ deprecated aliases + warn validation + sharedisModuleNotFoundError - cloud AuthPlugin should be loaded in both server and MSW modes, and gracefully handle missing HTTP server (support universal auth endpoints simulation) #862 — token migration
aiStudio/aiSeat→ai-studio/ai-seat - cloud feat: universal AuthPlugin — graceful HTTP server handling, mock auth fallback, and browser-safe runtime #863 — bump
.framework-sha+ adopt the shared@objectstack/spec/@objectstack/typeshelpers (one implementation) - framework feat(spec): reject unknown
requirescapability tokens at authoring (#3265) #3302 — unknownrequirestoken → harddefineStackerror (terminal state, Prime Directive Add comprehensive test suite for Zod schema validation #12)
(This traces back to #1597 / #3228, which made optional-plugin loading intent-driven and first exposed the divergence.)
Remaining, tracked separately: the time-gated removal of the
aiStudio/aiSeatdeprecated aliases after one release cycle → #3308.
Generated by Claude Code
- framework feat(spec,cli): one platform capability vocabulary — canonical kebab tokens, deprecated aliases, warn-first validation (#3265) #3281 — spec-owned
- added a commit that references this issue
on Jul 20, 2026 - added a commit that references this issue
on Oct 9, 2026
Context
Surfaced while finishing #1597 (#3228) and bumping cloud's framework pin (objectstack-ai/cloud#857). #1597 made framework's standalone
os serve/os startpath (packages/cli/src/commands/serve.ts) load optional service plugins by declared intent —requires→ required (fail-fast if the provider package is missing), declared-as-dep → auto (best-effort), otherwise skip with no speculative import; tier gating stays an orthogonal deny.While verifying the cloud side, I found the multi-tenant control plane does not boot through
serve.ts— it uses its ownpackages/objectos-runtime/src/capability-loader.ts, which already implements the same intent model independently. So there are now two implementations of one contract.Finding 1 — two parallel implementations of the same contract
serve.ts:CAPABILITY_PROVIDERS,CAPABILITY_TO_TIER, the AI block, and therequiresresolver loop (now intent-driven after feat(cli): make optional-plugin loading intent-driven, fail-fast on declared-but-missing (#1597) #3228).objectos-runtime/capability-loader.ts: its ownCAPABILITY_PROVIDERSmap withoptionalflags,failedRequired+probeRequiredCapabilities(loud on required-missing, quiet on optional-missing), and it already classifies the ESMCannot find packageshape (the perf(build): OS_SKIP_DTS gating + fix optional AI plugin "Cannot find package" skip #1595 fix).They encode the same rules but can drift: #1597 had to bring framework up to what cloud already did. A future fix to one won't automatically reach the other — declared ≠ enforced across the repo boundary (Prime Directive #10).
Finding 2 — divergent capability tokens (
ai-studiovsaiStudio)requiresuses kebab-case; feat(cli): make optional-plugin loading intent-driven, fail-fast on declared-but-missing (#1597) #3228 addedai-studio(matchingpinyin-search,hierarchy-security).defaultRequires: ['ai','aiStudio',…](apps/objectos/objectstack.config.ts),requires: ['aiStudio'](capability-loader.test.ts), and the provider keyaiStudioincapability-loader.ts.So
requires: ['ai-studio']is honored by framework's serve path but ignored by cloud's runtime, and['aiStudio']vice-versa. For an app author moving between the standalone and cloud runtimes, the same declaration doesn't mean the same thing — a one-contract violation (Prime Directive #12). Note that unknownrequirestokens are silently ignored on both sides, so the mismatch fails silently rather than loudly.Not a runtime bug today
Both loaders currently behave correctly on their own path (framework after #3228; cloud already). apps/cloud still boots clean (
aiStudioisoptional: true→ quiet skip, cloud#107). This is a maintainability / contract-consistency concern, not an outage — filed rather than silently expanded into #1597's scope.Proposed directions (decision needed)
serve.tsandobjectos-runtime, so there is structurally one implementation. Cloud's version is the more complete starting point (optional flags,failedRequired,probeRequiredCapabilities).requirestokens against a shared allowlist so an unknown/misspelled token is rejected at authoring instead of silently ignored.Evidence
packages/cli/src/commands/serve.ts(post-feat(cli): make optional-plugin loading intent-driven, fail-fast on declared-but-missing (#1597) #3228):CAPABILITY_TO_TIER['ai-studio'] = 'ai',Serve.resolveOptionalPluginLoad(...), thefor (const cap of requires)resolver loop.packages/objectos-runtime/src/capability-loader.ts:CAPABILITY_PROVIDERS.aiStudio { optional: true },failedRequired,probeRequiredCapabilities, ESMCannot find packageclassification.apps/objectos/objectstack.config.ts:defaultRequires: ['ai', 'aiStudio', 'analytics', 'automation', 'triggers'].