Skip to content

feat(cli): make optional-plugin loading intent-driven, fail-fast on declared-but-missing (#1597) - #3228

Merged
os-zhuang merged 2 commits into
mainfrom
claude/optional-plugin-intent-driven-fh2azp
Jul 18, 2026
Merged

feat(cli): make optional-plugin loading intent-driven, fail-fast on declared-but-missing (#1597)#3228
os-zhuang merged 2 commits into
mainfrom
claude/optional-plugin-intent-driven-fh2azp

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes#1597. Follow-up to #1595 (which fixed the Cannot find package false alarm) and the deeper design problem it exposed.

Problem

os serve auto-loaded the optional AI service plugins (@objectstack/service-ai, @objectstack/service-ai-studio) by speculatively importing the package and swallowing the error, conflating two distinct questions:

  1. "is the package installed?" — presence detection via try-import
  2. "does this app actually want AI?" — intent

Consequences: installing a dependency silently auto-enabled a runtime feature; nothing ever failed loud (an app that genuinely requires AI but ships without the package booted "successfully" in a broken state); and the single catch couldn't distinguish "intentionally absent" from "plugin crashed at startup" — the string-matching that caused #1595.

Design — intent-driven, three states

Enablement is now driven by declared intent, resolved deterministically at startup. Tier gating stays an orthogonal deny.

app declarespackage presentbehavior
required (requires: ['ai' | 'ai-studio'])yesload
requirednothrow — fail startup (fail-fast)
auto (declared in app package.json)yesload (opt-in convenience)
autonoskip — no speculative import
off (not declared / tier denies)never load

Key changes

  • Serve.resolveOptionalPluginLoad({ tierAllowed, required, declared }) — a pure, unit-tested resolver returning 'required' | 'auto' | 'off'. Both the AIService and AIStudio guards map over it.
  • Fail-fast for required capabilities. A capability the app explicitly declared in requires whose provider package is missing (or whose plugin throws while starting) now aborts boot with a clear message (the outer boot catch prints it and exits 1) instead of console.warn-ing and booting degraded. Declared caps are snapshotted before the platform auto-injects its convenience defaults (ALWAYS_ON, mcp, pinyin-search, auth→email, queue/job), which stay best-effort.
  • AI Studio gets a first-class requires: ['ai-studio'] required path (maps to the ai tier; implies the base service). An undeclared Studio is no longer speculatively imported at all.
  • No more message-matching to tell missing from crashed. Consolidated into one Serve.isModuleNotFoundError (checks err.code first — the perf(build): OS_SKIP_DTS gating + fix optional AI plugin "Cannot find package" skip #1595 fix), replacing every duplicated string-match in serve.ts (AI ×2, capability resolver, auth, datasource ×2).
  • Spec: documented the intent / fail-fast contract on the requires field (stack.zod.ts).

Constraint preserved — apps/cloud boots clean (cloud#107)

apps/cloud ships no AI Studio and must boot clean. It declares neither the Studio package nor requires: ['ai-studio'], so Studio resolves to off → skipped, no import, no error. This is strictly better than before: previously an app declaring only the base service still speculatively imported Studio and swallowed the failure.

Why this is CI-safe

@objectstack/cli depends on every built-in capability provider package (service-automation, plugin-approvals, service-job, service-messaging, service-package, trigger-*, …), so bare import(spec.pkg) always resolves wherever the CLI runs. The example apps' declared capabilities all resolve today, so fail-fast never wrongly fires. service-ai / service-ai-studio are external/private — exactly where declared-but-missing should fail fast.

Verification

  • tsc -p packages/cli/tsconfig.build.json --noEmitexit 0
  • New packages/cli/test/serve-optional-plugin-intent.test.ts (intent resolver + isModuleNotFoundErrorperf(build): OS_SKIP_DTS gating + fix optional AI plugin "Cannot find package" skip #1595 regression guard) → 9 passing
  • serve-defaults, serve-host-config, serve-log-level, serve-automation-summary, adr-0048-app-splitall passing
  • packages/specstack.test.ts89 passing; full spec build succeeds
  • eslint on changed files → clean

🤖 Generated with Claude Code


Generated by Claude Code

…1597)
`os serve` auto-loaded the optional AI service plugins by speculatively
importing the package and swallowing the error, conflating two things:
"is the package installed?" (presence) and "does this app want AI?" (intent).
Nothing ever failed loud — an app that genuinely REQUIRES AI but ships without
the package booted "successfully" in a broken state, and the single catch
couldn't tell "intentionally absent" from "plugin crashed at startup" (the
#1595 false-alarm class).
Drive enablement from declared INTENT instead, resolved deterministically at
startup into three states (tier gating stays an orthogonal deny):
required (requires: ['ai'|'ai-studio']) load; missing/broken => fail-fast (throw, exit 1)
auto (package declared in app pkg) best-effort load
off (neither, or tier denies) skip, with NO speculative import
- AIService + AIStudio guards resolve via `Serve.resolveOptionalPluginLoad`
(pure, unit-tested). Studio gains a `requires: ['ai-studio']` required path
(opens the `ai` tier, implies the base service).
- The `requires: [...]` capability resolver now fails fast for capabilities the
app EXPLICITLY declared; platform-injected defaults (ALWAYS_ON, mcp, ...) stay
best-effort. Declared caps are snapshotted before auto-injection.
- Missing-vs-crashed detection consolidated into one `Serve.isModuleNotFoundError`
(checks err.code first — the #1595 fix), replacing the duplicated string-matches.
- apps/cloud constraint preserved: no AI Studio declared => skipped, clean boot
(cloud#107); an undeclared Studio is no longer speculatively imported.
Spec: document the intent / fail-fast contract on `requires`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjHfkKmEvgk8v7N8nTe5sH
@vercel

vercelBot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 18, 2026 4:48pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/spec.

108 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via packages/cli, @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/api/data-flow.mdx(via @objectstack/cli)
  • content/docs/api/environment-routing.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via packages/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/cli, packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/backup-restore.mdx(via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx(via @objectstack/cli)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/cli.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via packages/cli)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/cli, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/permissions/authentication.mdx(via @objectstack/cli)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx(via @objectstack/cli)
  • content/docs/protocol/kernel/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…#1597)
`os start` "What it boots" listed `requires: [...]` auto-registration but
predated the intent-driven fail-fast behavior. Clarify that a declared
service capability whose provider package isn't installed aborts boot
(fail-fast), and call out that `auth`/`ui` are the tier-gated exceptions
with their own opt-in rules.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjHfkKmEvgk8v7N8nTe5sH
@os-zhuang
os-zhuang marked this pull request as ready for review July 18, 2026 15:58
@github-actionsgithub-actionsBot added the documentation Improvements or additions to documentation label Jul 18, 2026
@os-zhuang
os-zhuang merged commit 1b733db into mainJul 18, 2026
16 of 17 checks passed
@os-zhuang
os-zhuang deleted the claude/optional-plugin-intent-driven-fh2azp branch July 18, 2026 16:08
os-zhuang pushed a commit that referenced this pull request Jul 18, 2026
…he MCP endpoint on `os dev` boot (#3167)
#3167 PR-B — the direction-ratified, non-overlapping half. The HTTP
identity-admission e2e proof landed separately (#3228: showcase-based,
HIGH-RISK); this rebases onto it and keeps only what that PR did not cover.
Decision 2 (off-switch semantics): the MCP HTTP surface and the long-lived
stdio transport shared one env var — OS_MCP_SERVER_ENABLED=true turned HTTP on
AND silently auto-started the UNSCOPED stdio bridge (raw services, no
per-request principal). Split them:
- types: new resolveMcpStdioAutoStart(); stdio is now OS_MCP_STDIO_ENABLED
(default off). OS_MCP_SERVER_ENABLED governs only HTTP. Legacy
OS_MCP_SERVER_ENABLED=true still starts stdio for one release, flagged
deprecated (=false only ever gated HTTP, unchanged).
- mcp: plugin.start() gates stdio on the new switch + warns once on the legacy
alias. The bridgeResources(...) line (mcp-stdio-authority probe key) is
untouched; the matrix row's env-var references are synced.
Decision 3 (dev affordance): `os dev` prints the MCP endpoint, skill URL, and a
ready-to-paste `claude mcp add` command on boot, gated on isMcpServerEnabled()
(opted-out deployments advertise nothing).
Decision 4 (exposure policy): documented the current default (non-system
objects + ai.exposed actions over the ~10-tool spine) as the committed v1
policy in connect-mcp.mdx, with the metadata-authorable direction noted.
Also: env-var docs updated for the split (+ deprecation callout); the blank
scaffold README documents the serve-side MCP surface; unit tests pin the new
switch semantics (canonical / legacy-deprecated / footgun-closed). Changeset
covers types+mcp+cli (minor) and create-objectstack (patch).
Refs #3167, #3228, ADR-0096, ADR-0097.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115eg8dAaCfWaDYYAm3ma36
os-zhuang added a commit that referenced this pull request Jul 18, 2026
…t UX + exposure-policy docs (#3217)
Decouple the MCP stdio auto-start switch (OS_MCP_STDIO_ENABLED) from the default-on HTTP surface (OS_MCP_SERVER_ENABLED), closing the footgun where =true silently attached an unscoped stdio bridge; print the MCP endpoint + connect command on `os dev` boot; document the v1 exposure policy and the env-var split.
The HTTP identity-admission e2e proof landed separately via #3228; this is the de-duplicated remainder of #3167 PR-B.
Refs #3167, #3228, ADR-0096, ADR-0097.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mtests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make optional-plugin loading intent-driven: fail-fast on declared-but-missing, drop presence-based auto-enable

2 participants

@os-zhuang@claude