Repository navigation
Closed
Labels
bugSomething isn't workingSomething isn't workingdomain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loopReady for the PM dispatch loop
Description
发现于 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())。所以插件在构造/挂载阶段抛出的任何错误——不只是模块解析失败——都会:
- 被打印成 「@objectstack/organizations could not be loaded」,即「包缺席」;
- 附带 「set OS_ALLOW_DEGRADED_TENANCY=1」 作为一条出路;
- 在该 env 已设时,被降级为一条 warning 并继续启动。
为什么这是个问题
cloud 的多组织运行时(私有包 @objectstack/organizations)现在会在构造时对未授权抛错(企业许可闸门)。这与「包缺席」是两件事,解法完全不同:
| 包缺席 | 未授权 | |
|---|---|---|
| 事实 | 运行时不在这台机器上 | 运行时就在镜像里,但这个部署没被授权用它 |
| 解法 | 装上它 / 改单组织 / 显式接受降级 | 拿一张许可 / 改单组织 |
OS_ALLOW_DEGRADED_TENANCY=1 该不该管用 |
该(operator 明确接受能力缺席) | 不该(那是授权问题,共享逃生口等于把许可闸门搬到一个 env 变量上) |
合并之后有两个具体后果:
- 运维排错跑偏:包明明在镜像里,日志却说它「加载不出来」,第一反应是去查模块解析 / NODE_PATH / prune,而真因是授权。
- 逃生口串味:
OS_ALLOW_DEGRADED_TENANCY=1会把「未授权」也吞成 warning 继续启动。它的语义只应是「包加载不出来但我接受降级」。
建议
只改错误分类,不动 D5 的态度(要求了隔离就不能假装有,仍然拒绝启动):
- 把
import与kernel.use(...)拆成两段 try。前者失败 = 包缺席,走现有 D5 文案与OS_ALLOW_DEGRADED_TENANCY逃生口;后者失败 = 插件自己拒绝,属于另一类。 - 对插件抛出的、带结构化标识的错误原样上报并无条件退出,不套用「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
Assignees
Labels
bugSomething isn't workingSomething isn't workingdomain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loopReady for the PM dispatch loop
Activity
xuyushun441-sys commentedon Aug 3, 2026
认领:PM 循环第 10 轮
会话:
session_018iARDqtrhQgz6fVHDeDkbQ分支:
claude/issue-4818-split-organizations-import-from-mountWorktree:
objectstack-issue-4818车道:
domain:cli—— 维护者已在发版优先期批准本会话在domain:devx之外加认领该域,登记 #4604。文件面声明(供其他 PM 的全局在飞检查):
packages/cli/src/commands/serve.ts+ 其旁测试。全局在飞检查:已列全仓
pm:dispatched,无一触碰serve.ts。主 PM 在飞的 #4700(@objectstack/verify裸 import)、#4829(restfilterAppForUser)按认领协议归其收尾,与本单文件面不相交。前提复核(对最新
main):缺陷仍在,逐字如 issue 所述 ——serve.ts:1760的importFromHost(organizationsPkg)与:1761的kernel.use(new mod.OrganizationsPlugin())在同一个try内,catch (orgErr)在:1763,D5 文案与OS_ALLOW_DEGRADED_TENANCY逃生口在:1789。为什么排在本域首位:发版后运维第一眼看到的就是启动错误。一条误导性的错误信息比没有错误更贵 —— 它把人指向模块解析 / NODE_PATH / prune,而真因是授权;更糟的是
OS_ALLOW_DEGRADED_TENANCY=1会把「插件拒绝挂载」也吞成 warning 继续启动,等于把一道许可闸门搬到一个 env 变量上。方案取 issue 推荐的那一条(中性判据):import 阶段失败 → 归 D5(现有文案 + 逃生口不变);插件构造/挂载阶段抛出的错误一律原样上报并无条件退出,不套「could not be loaded」文案、不认
OS_ALLOW_DEGRADED_TENANCY。不要引入err.code === 'MULTI_ORG_NOT_LICENSED'或isMultiOrgLicenseError这类判据 —— framework 不该知道任何 cloud 私有语义,而且该包是importFromHost动态加载的,CLI 与它可能持有不同模块实例,instanceof与具名 code 都是脆的。按阶段分类是唯一不需要 framework 了解插件内部的切法。D5 的态度不变:要求了隔离就不能假装有,仍然拒绝启动。
Generated by Claude Code
xuyushun441-sys commentedon Aug 3, 2026
审核通过 — PR #4858 已取消草稿并挂上 auto-merge(SQUASH)
三条要害都核过了实物,不是只读 PR 描述:
判据轴 = 阶段,不是错误形状。 我在 diff 里搜了 cloud 私有谓词:没有
MULTI_ORG_NOT_LICENSED,没有isMultiOrgLicenseError。三处instanceof都是良性的 —— 两处是Error收窄以取message,一处在解释性注释里,另有一处是改动前就在的 driver 代码。这一点是本 issue 最容易做错的地方:按错误的code/类型分类看起来更"精确",实则要求 framework 编码 cloud 的私有语义,而且该包是importFromHost动态加载的,CLI 与插件可能持有不同模块实例,instanceof和具名 code 都靠不住。"哪个阶段抛的"是唯一不需要 framework 了解插件内部的切法,这条推理已写进serve.ts的注释里留给下一个人。包缺席那条路分毫未动。 同一条 ADR-0093 D5 文案、同一个
OS_ALLOW_DEGRADED_TENANCY=1逃生口,而且用例 2 把这个行为钉住了 —— 不是"我没改",是"改了就会红"。测试是有承重的。 反向验证(把
serve.tsstash 回改前)让挂载失败的两个用例失败、import 失败的两个用例通过。只观察到绿的测试和一个什么都断言不到的测试,从外部无法区分 —— 这条要求本会话已经吃过教训(#4690、#4804),不再接受单向证明。落点纯净:3 个文件,全在
packages/cli/**与.changeset/,零文件越界到其他 domain,未触碰content/docs/releases/。CI 18 项无红,auto-merge 已挂;进队合入后本 issue 自动关闭。
Generated by Claude Code
fix(cli): classify organizations failure by STAGE — import ≠ mount (o…