Skip to content

fix(cli): resolve @objectstack/organizations from the host app (cloud#1013) - #4699

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-1013-organizations-import-from-host
Aug 2, 2026
Merged

fix(cli): resolve @objectstack/organizations from the host app (cloud#1013)#4699
os-zhuang merged 2 commits into
mainfrom
claude/issue-1013-organizations-import-from-host

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Fixes objectstack-ai/cloud#1013

问题

objectstack serve裸 import 加载企业版多组织运行时:

const organizationsPkg = '@objectstack/organizations';
const mod: any = await import(/* webpackIgnore: true */ organizationsPkg);

Node ESM 对裸 specifier 按导入方自己的 realpath 解析 —— CLI 是通过 workspace/link: 接入的,realpath 落在 framework 工作区里;而 @objectstack/organizations 是 cloud 私有包,只存在于被服务的 appnode_modules。这个 import 因此永远不可能成功:

cause: Cannot find package '@objectstack/organizations' imported from …/packages/cli/src/commands/serve.ts

后果是任何自托管的 walled posture(OS_TENANCY_POSTURE=group / isolated)都直接撞上 ADR-0093 D5 的 fail-fast 并 exit(1),唯一绕过方式是 OS_ALLOW_DEGRADED_TENANCY=1 —— 正是 D5 要防的「墙不生效还照常服务」状态。在 app 的 package.json 里声明依赖没有任何用,因为解析基点根本不是 host app。

修复

同一个文件里本来就有正确的解析器 importFromHost(先用 host app 的 createRequire 解析,再 import 解析出的绝对路径),但它定义在 auth 区块之后,而 organizations 的加载在该区块之内,所以用不上。

  • 把它抽成 packages/cli/src/utils/import-from-host.ts(createHostRequire / createHostImporter),上提到 auth 区块之前,organizations 处改用它。AI 服务那条路径行为不变(仍是同一个 helper)。
  • host app 只要在自己的 package.json 里声明 @objectstack/organizations 即可 —— 两个 showcase app 已经声明了。

顺带两处修正:

  1. 回退只在「host 解析不到」时发生。 原实现 try { host } catch { bare } 会把已解析成功但求值时抛错的包重新裸 import 一遍,真实错误被替换成 MODULE_NOT_FOUND —— 而所有调用方都把它归类为「没装」(Serve.isModuleNotFoundError)。于是一个坏掉的包会被静默跳过(可选服务),或者让 D5 打印「请安装」去指责一个已经装好的包。现在求值期的崩溃原样抛出。
  2. D5 的致命信息指明「装到 app 里」,因为那才是包该待的地方。

回归 —— 这是本 PR 的重点

这个缺陷能活这么久,是因为所有多组织测试都绕开了 CLI 自己的解析路径:cloud 的 dogfood 套件显式传 extraPlugins: [new OrganizationsPlugin()],packages/verify 的 posture 测试直接 vi.mock 掉这个模块。两者都跳过了唯一坏掉的那一环。

新增 packages/cli/test/serve-organizations-host-resolution.e2e.test.ts:用现成的 test/helpers/serve-process.ts spawn 真实的 os serve 进程,跑在一个临时 app 目录上 —— 真的 package.json、真的 node_modules/@objectstack/organizations(企业包在本工作区装不了,这正是问题本身,所以用一个注册同样 org-scoping 服务 + posture entitlement 的替身),不 mock 任何东西

  • case 1(核心):app 自带该包 + OS_TENANCY_POSTURE=isolated ⇒ 必须启动到 banner,且插件列表里出现 Organizations
  • case 2:app 没有该包 ⇒ D5 fail-fast 仍然必须触发(修复不能把它变成宽容跳过)。

对旧代码验证过会红:把那一行改回裸 import 后重跑,case 1 失败并打印出 issue 里那条一模一样的信息:

AssertionError: the D5 fail-fast fired — the app-installed package was not found
  ✖ FATAL: tenancy posture 'isolated' was requested but @objectstack/organizations could not be loaded,
  cause: Cannot find package '@objectstack/organizations' imported from …/packages/cli/src/commands/serve.ts

另加 packages/cli/src/utils/import-from-host.test.ts —— 用真实 fixture 目录钉住解析语义:issue 的两半 repro(CLI 侧解析不到 / host 侧解析得到)、回退分支、以及「求值崩溃不得被伪装成 MODULE_NOT_FOUND」。

文档

content/docs/deployment/tenancy-modes.mdx 的 D5 boot-guard 补救清单原文是「install @objectstack/organizations」—— 在修复前这句根本无法执行(装在哪都没用,因为解析基点是 CLI 的 realpath);修复后位置变成精确且是关键信息,所以那一条改为「装进被服务的 app,在该 app 的 package.json 里声明」,措辞与 CLI 打印的 D5 致命信息一致。只改这一条,不做文档普扫。

验证

$ pnpm exec vitest run --maxWorkers=2          # packages/cli 全量
  Test Files  70 passed (70)
       Tests  711 passed (711)

$ pnpm --filter @objectstack/cli typecheck      # tsc --noEmit,干净
$ pnpm exec turbo run build --filter=@objectstack/cli... --concurrency=2
  Tasks:  54 successful, 54 total
$ pnpm exec eslint --no-inline-config <改动的文件>   # 干净

顺带扫描到的同类缺陷(不在本 PR 范围,已另开 issue #4700)

packages/verify/src/harness.ts:270bootStack({ multiTenant: true }) 是同一形状的裸 import,同样解析不到;packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts:528 用裸 import 探测该包是否可用,因而永远判定「不可用」。修它需要先决定这个 node-only 解析器的共享位置(CLI 的 utils 用不了,@objectstack/types 会把 node:module 带进可能跑在 edge/浏览器的包里),属于跨包架构决定 —— 详见 #4700

另注(未改、未开 issue,留给维护者判断):tenancy-modes.mdx 里那段引用的 FATAL 代码块仍是 OS_MULTI_ORG_ENABLED=true but … 的旧措辞,而 CLI 现在打印的是 ADR-0105 D1 的 posture 措辞(tenancy posture 'isolated' was requested but …)。这是本次改动之前就存在的漂移,按 PM 指示本 PR 只动那一条 bullet,未一并修改。

Refs: ADR-0093 D5、ADR-0081 D2、ADR-0105 D12、cloud#1012

🤖 Generated with Claude Code

https://claude.ai/code/session_019TYoxKa8yFLiDDh7tkBqtu

…#1013)

`objectstack serve` loaded the enterprise multi-org runtime with a bare
`import('@objectstack/organizations')`. Node ESM resolves a bare specifier
against the importer's own realpath — the CLI's, inside the framework
workspace it is linked out of — while that package is cloud-private and
lives in the served app's node_modules. The import could therefore never
succeed: every self-hosted deployment requesting a walled tenancy posture
(`group` / `isolated`) hit the ADR-0093 D5 fail-fast and exited 1, and the
only way past it was OS_ALLOW_DEGRADED_TENANCY=1 — the unwalled state D5
exists to prevent.

The file already had the right resolver (`importFromHost`), but it was
declared AFTER the auth block that contains this load, so the organizations
site could not use it. Extracted to `src/utils/import-from-host.ts`
(`createHostRequire` / `createHostImporter`), hoisted above the auth block,
and used at the organizations site.

Two adjacent corrections:

- the fallback now applies only when the host cannot RESOLVE the package. A
  host-resolved package that throws while it evaluates propagates its real
  error instead of being re-imported bare and reported as MODULE_NOT_FOUND,
  which every caller classifies as "not installed".
- the D5 fatal names the app as where the package must be installed.

Regression: `test/serve-organizations-host-resolution.e2e.test.ts` spawns the
REAL `os serve` against a temp app that carries the package in its own
node_modules. Every existing multi-org test bypasses this path (cloud's
dogfood suites pass `extraPlugins: [new OrganizationsPlugin()]`; the verify
harness posture test mocks the module), which is why the defect survived. The
new e2e fails against the bare import with the exact issue message and passes
with the fix; a second case pins that the D5 fail-fast still fires when the
app genuinely lacks the package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TYoxKa8yFLiDDh7tkBqtu
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 2, 2026 7:04pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli.

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

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/validating-metadata.mdx (via packages/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/plugins/index.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli)
  • content/docs/releases/v16.mdx (via @objectstack/cli)

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.

…st be installed (cloud#1013)

The D5 boot-guard remediation list said "install @objectstack/organizations",
which was un-actionable while the CLI resolved the package against its own
realpath — installing it ANYWHERE did not lift the guard. Now that `serve`
resolves from the host app, the location is exact and load-bearing, so the
bullet says it: declare it in the served app's package.json. Same wording as
the fatal message the CLI prints, so the doc and the terminal agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TYoxKa8yFLiDDh7tkBqtu
@github-actions github-actions Bot added size/l and removed size/m labels Aug 2, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 2, 2026 19:40
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 2, 2026
Merged via the queue into main with commit 16fc124 Aug 2, 2026
23 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-1013-organizations-import-from-host branch August 2, 2026 19:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants