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 intentrequires → 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'].

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions