Skip to content

serve.ts 的 ADR-0093 D5 catch 把 OrganizationsPlugin 抛出的任何错误都改写成「包加载不出来」,并给出会误导的 OS_ALLOW_DEGRADED_TENANCY 解法 #4818

Description

@xuyushun441-sys

发现于 cloud 侧实现 objectstack-ai/cloud#1020(多组织许可闸门归位到插件自身)。不改本条不阻塞那边——cloud 已用「在 app config 里更早拒绝」绕开——但 framework 这一侧的守卫确实把两种不同的失败合并成了一种,属于 ADR-0049「声明与执行不一致」那一类。

现象

packages/cli/src/commands/serve.ts(当前 main,约 1745-1810 行):

const tenancyPosture = resolveTenancyPosture();
const multiTenant = tenancyPosture !== 'single';
if (multiTenant) {
  try {
    const mod: any = await importFromHost('@objectstack/organizations');
    await kernel.use(new mod.OrganizationsPlugin());   // <- 也在 try 里
    trackPlugin('Organizations');
  } catch (orgErr) {
    if (!resolveAllowDegradedTenancy()) {
      console.error(chalk.red(
        `\n  ✖ FATAL: tenancy posture '${tenancyPosture}' was requested but ` +
        '@objectstack/organizations could not be loaded,\n' + ...
        '      • set OS_ALLOW_DEGRADED_TENANCY=1 to boot in an explicitly degraded single-org state.\n\n'
      ));
      process.exit(1);
    }
    console.warn(chalk.yellow('  ⚠ DEGRADED TENANCY (OS_ALLOW_DEGRADED_TENANCY=1) ...'));
  }
}

这个 catch 同时覆盖 importFromHost(...) kernel.use(new mod.OrganizationsPlugin())。所以插件在构造/挂载阶段抛出的任何错误——不只是模块解析失败——都会:

  1. 被打印成 「@objectstack/organizations could not be loaded」,即「包缺席」;
  2. 附带 「set OS_ALLOW_DEGRADED_TENANCY=1」 作为一条出路;
  3. 在该 env 已设时,被降级为一条 warning 并继续启动

为什么这是个问题

cloud 的多组织运行时(私有包 @objectstack/organizations)现在会在构造时对未授权抛错(企业许可闸门)。这与「包缺席」是两件事,解法完全不同:

包缺席 未授权
事实 运行时不在这台机器上 运行时就在镜像里,但这个部署没被授权用它
解法 装上它 / 改单组织 / 显式接受降级 拿一张许可 / 改单组织
OS_ALLOW_DEGRADED_TENANCY=1 该不该管用 该(operator 明确接受能力缺席) 不该(那是授权问题,共享逃生口等于把许可闸门搬到一个 env 变量上)

合并之后有两个具体后果:

  • 运维排错跑偏:包明明在镜像里,日志却说它「加载不出来」,第一反应是去查模块解析 / NODE_PATH / prune,而真因是授权。
  • 逃生口串味OS_ALLOW_DEGRADED_TENANCY=1 会把「未授权」也吞成 warning 继续启动。它的语义只应是「包加载不出来但我接受降级」。

建议

只改错误分类,不动 D5 的态度(要求了隔离就不能假装有,仍然拒绝启动):

  1. importkernel.use(...) 拆成两段 try。前者失败 = 包缺席,走现有 D5 文案与 OS_ALLOW_DEGRADED_TENANCY 逃生口;后者失败 = 插件自己拒绝,属于另一类。
  2. 对插件抛出的、带结构化标识的错误原样上报并无条件退出,不套用「could not be loaded」文案,也不认 OS_ALLOW_DEGRADED_TENANCY。判定建议用结构化字段而非 instanceof——该包是 importFromHost 动态加载的,CLI 与它可能持有不同模块实例。cloud 侧已经导出了这种形状(err.code === 'MULTI_ORG_NOT_LICENSED',以及一个 isMultiOrgLicenseError(err) 谓词),framework 只需一个通用判据即可,例如「错误自带 code 且插件包导出了对应谓词」,或更中性的「插件抛出的错误一律原样上报,只有 import 阶段的失败才归入 D5」。后者更简单,也不需要 framework 知道任何 cloud 私有语义——推荐这一条

兼容性

第 2 条只影响「插件存在但挂载失败」这条路径,今天它本来就走 D5 退出(除非设了 degraded)。变化是:文案变准确,且 degraded 逃生口不再能吞掉插件自己的拒绝。包真缺席的路径行为完全不变。

关联

  • objectstack-ai/cloud#1020(许可闸门归位插件;cloud 侧已合的规避是在 app config 里、插件挂载之前先拒绝)
  • ADR-0093 D5(降级即拒绝启动)、ADR-0049(声明与执行不一致)、ADR-0022(离线许可 / license is deterrence, not DRM)

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions