Skip to content

Unify optional-plugin loading: framework serve.ts and cloud objectos-runtime are parallel implementations with divergent capability tokens (ai-studio vs aiStudio) #3265

Description

@os-zhuang

Context

Surfaced while finishing #1597 (#3228) and bumping cloud's framework pin (objectstack-ai/cloud#857). #1597 made framework's standalone os serve / os start path (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 own packages/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

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-studio vs aiStudio)

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 unknown requires tokens 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 (aiStudio is optional: 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)

  1. Extract a shared loader (preferred long-term): lift the capability-provider registry + required/optional resolution into a framework-owned module consumed by both serve.ts and objectos-runtime, so there is structurally one implementation. Cloud's version is the more complete starting point (optional flags, failedRequired, probeRequiredCapabilities).
  2. At minimum, reconcile the token vocabulary: pick one canonical spelling for the AI-Studio capability (and any other multiword tokens), alias the other through a deprecation path, and validate requires tokens against a shared allowlist so an unknown/misspelled token is rejected at authoring instead of silently ignored.
  3. Or document a deliberate split: if standalone vs multi-tenant genuinely need different loading, document the boundary + token mapping so it reads as intentional, not accidental drift.

Evidence

  • framework 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(...), the for (const cap of requires) resolver loop.
  • cloud packages/objectos-runtime/src/capability-loader.ts: CAPABILITY_PROVIDERS.aiStudio { optional: true }, failedRequired, probeRequiredCapabilities, ESM Cannot find package classification.
  • cloud apps/objectos/objectstack.config.ts: defaultRequires: ['ai', 'aiStudio', 'analytics', 'automation', 'triggers'].

Activity

  1. os-zhuang commented on Jul 19, 2026

    @os-zhuang
    ContributorAuthor

    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 with pinyin-search / hierarchy-security; requires is a spec-owned contract field, so the spec's convention wins). aiStudio / aiSeat become deprecated aliases for one release.

    Implementation:

    • framework#3281 (draft): spec-owned PLATFORM_CAPABILITY_TOKENS + DEPRECATED_PLATFORM_CAPABILITY_ALIASES + canonicalizePlatformCapability; defineStack rewrites aliases at authoring time and warns on unknown tokens (warn-first → intended to become an error once the vocabulary proves complete); serve.ts canonicalizes 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.ts keys → ai-studio/ai-seat with 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

  2. self-assigned this
    on Jul 19, 2026
  3. os-zhuang commented on Jul 19, 2026

    @os-zhuang
    ContributorAuthor

    Done — closing as completed

    Both findings are resolved and all work is merged. The framework CLI serve path and cloud's objectos-runtime now share one capability vocabulary and one implementation of the canonicalization + module-not-found logic, so the parallel-loader drift and the ai-studio/aiStudio divergence are structurally gone.

    Merged PRs:

    (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/aiSeat deprecated aliases after one release cycle → #3308.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions