Skip to content

[Decision] Should the cloud control-plane contracts leave @objectstack/spec and live in the cloud repo? (upstream of #12036) #16325

Description

@hotlong

Maintainer direction given in a direct PM session on 2026-09-06, verbatim (⛔ quoted, not translated):

我一直觉得 cloud 的协议应该放在云端,没必要开源

Filed as a decision card on the maintainer's instruction in the same session ("开决策卡"). This card is
UPSTREAM of #12036: if the control-plane schemas leave the open-source contract package, #12036's
camelCase-vs-snake_case fork stops being a cross-repo contract decision and becomes one repo's internal
coding style. #12036 is therefore moved out of the decision inbox and blocked on this card.

⛔ Nothing was changed. No branch, no edit. All readings below are read-only.

Readings

Taken 2026-09-06T04:42Z. objectstack-ai/objectstack origin/main d5d8d50db;
objectstack-ai/cloud origin/main daaac081; objectstack-ai/objectui origin/main 0558e0f78.

1. The boundary already exists — it was drawn half-way

@objectstack/spec's main entry does not re-export the cloud namespace. ./cloud is a standalone
published subpath in the package's exports map (packages/spec/package.json), with its own
browser/import/require conditions.

$ grep -cE "^export" packages/spec/src/index.ts          # control — the file does export things
59
$ grep -cE "^export.*cloud" packages/spec/src/index.ts   # term under test
0

⇒ 59 export lines and zero of them name cloud: the absence is real, not a broken pattern. Reaching these
schemas already requires the explicit @objectstack/spec/cloud subpath.

2. The open-source repo's real dependency on the control-plane half is ONE symbol

Every other in-repo occurrence of @objectstack/spec/cloud outside packages/spec/src/cloud/ is a
comment, a test, or a re-export of something that lives elsewhere:

site what real dependency?
packages/spec/src/api/discovery.zod.ts:8 import type { EnvironmentType } from '../cloud/environment.zod' YES — the only one. A string-union type
packages/spec/src/api/package-api.zod.ts:10 import { ArtifactReferenceSchema } from '../cloud/marketplace.zod' YES, but on the PACKAGE-FORMAT half (see §3)
packages/metadata/src/plugin.ts:895 dynamic import('@objectstack/spec/cloud') for EnvironmentArtifactSchema Nopackages/spec/src/cloud/environment-artifact.zod.ts is a pure re-export whose body is export { … } from '../system/environment-artifact.zod'. Re-point the import at @objectstack/spec/system and the dependency is gone
packages/client/src/index.ts:2233,2350 comments only No — see §4
packages/cli/test/package-publish-namespace.test.ts CreatePackageRequestSchema test, package-format half
packages/client/src/return-type-precision.test.ts Environment type test

3. spec/cloud holds two families with different owners

half files lines what it actually is
control-plane private environment.zod, environment-package.zod, tenant.zod, developer-portal.zod, marketplace-admin.zod, app-store.zod 2087 the cloud service's own row shapes and back-office. Open-source code touches one type from it
package & marketplace format package.zod, package-version.zod, marketplace.zod, package-l10n, template-manifest.zod 1400 ⚠️ not a cloud protocol. A package author needs it; the open-source CLI's os package publish path uses CreatePackageRequestSchema; spec/api/package-api.zod.ts depends on its ArtifactReferenceSchema

Intra-directory dependency roots: package.zodmarketplace.zod ← {developer-portal, app-store};
tenant.zodenvironment.zod. So the two halves are separable, with one edge to cut
(app-store and developer-portal sit on marketplace.zod).

4. The SDK already refuses to bind these — with a maintainer ruling on record

packages/client/src/index.ts:2228-2248 is a 20-line docblock stating that the 15 environments.* methods
keep their erased any deliberately, naming #11925/#12036 and the casing conflict. At :2340-2372 a
prior ruling is recorded verbatim — maintainer, 2026-08-29: 「同意」— option 甲 — choosing the INLINE wire
shape and ⛔ NOT binding @objectstack/spec/cloud's ProvisionEnvironmentResponseSchema.

⇒ The direction on this card has effectively been ruled once already, per-method. This card asks whether it
becomes the rule for the whole subpath.

5. Producer and consumers of the control-plane half both live in the cloud repo

From the repo:cloud reading already on #12036 (comment 5520459478, cloud 9b6abe0), re-confirmed at
daaac081: 12 import sites, including a runtime parse at
packages/service-tenant/src/environment-provisioning.ts:750
(ProvisionEnvironmentRequestSchema.parse(request)) and const environment: Environment = { … } at :1094
and :1429. The schemas' only real consumer is the repo they would move to.

6. objectui's dependency is one blanket re-export line

$ git grep -n "@objectstack/spec/cloud" origin/main   # objectstack-ai/objectui @ 0558e0f78
packages/types/src/index.ts:1106:export type * as Cloud from '@objectstack/spec/cloud';

Every other hit in that repo is a comment, and each of those comments says the type was deliberately
re-declared inline instead of imported
(app-shell/src/console/marketplace/marketplaceApi.ts:85,
usePackageL10n.ts:6, app-shell/src/views/metadata-admin/PackagesPage.tsx:75).

The options

A — move the whole ./cloud subpath to the cloud repo. All 3487 lines leave @objectstack/spec; the
subpath export is deleted.
⚠️ Cost: the package/marketplace format leaves with it, so os package publish and
spec/api/package-api.zod.ts lose their declarations — a package author would need a cloud-repo package to
author a package for the open-source CLI.

B — cut by owner (recommended). The control-plane half (2087 lines, 6 files) moves to the cloud repo.
The package/marketplace half (1400 lines, 5 files) STAYS in the open-source spec, relocated out of
src/cloud/ under a name that says what it is (package format, not cloud protocol). EnvironmentType is
re-declared in spec/api. packages/metadata re-points at @objectstack/spec/system. The ./cloud
subpath export is deleted.

C — no move. The boundary stays where it is, and the decision reverts to #12036's three-way casing fork
(spec moves to snake_case / the wire moves to camelCase / a transform is documented).

What B costs, stated plainly

  1. Deleting a published public subpath export is a breaking change to @objectstack/spec — major
    version plus a release note. Precedent exists on this exact API family: content/docs/releases/v17.mdx:218
    shipped "client.projects.* becomes client.environments.*, with no aliases".
  2. objectstack-ai/objectui packages/types/src/index.ts:1106 must drop or re-point its
    export type * as Cloud line before the subpath is removed.
  3. Ordering is a real constraint: cloud and objectui must stop importing BEFORE objectstack removes the
    export, because cloud consumes this through its .objectstack-sha pin. Three cards, not one PR.
  4. Whether a deprecation window is left is NOT reopened here: the maintainer's 2026-08-27 ruling (verbatim:
    「项目在创业阶段,用户也很少,短期不考虑渐进。」) makes immediate removal with no alias the default.
    Say so explicitly in the answer if a window is wanted instead.

Not a duplicate, and not in conflict with the open-core work

#16215 and #16130 move multi-organization RUNTIME capability INTO the open core. This card moves the
commercial CONTROL PLANE's contracts out of it. Same boundary, opposite sides — the framework grows, the
service's private shapes leave. Searched repo:objectstack-ai/objectstack state:open for spec/cloud
in:title (2 hits: #12036, #16084 — neither is this), cloud contracts boundary (7), and
open source boundary spec (81); no existing card asks this question.

推荐:B。 ①领起(把错放的搬走,让契约包里每样都真是契约);③让误绑在结构上不可能,而不是靠一段 docblock;④净收窄。②不反对,但要如实说它不紧急 —— 这不是 P0,客户今天不坏。
回退:C(不动)。 若你认为三仓窗口此刻不值得,C 是诚实的选择,代价是回到 #12036 的三选一,且窗口随消费者增加而变贵。
不荐 A。 它会把包/市场格式一起搬走,而 os package publish 是开源 CLI 的能力、包作者需要那个格式;A 会为了搬走 2087 行而误伤 1400 行。
置信缺口(本分析看不见什么): 未量 cloud 侧接收这 6 个文件的施工成本(它今天 import 的是已发布包,改成本地声明要动 12 个站点、含一处运行时 .parse())。⚠️ 这是执行排序的输入,不改变方向判断;若你选 B,该由 repo:cloud 席先出一份接收面读数再排卡。

Governing text: AGENTS.md Prime Directive #12"Contract-first — fix the metadata, not the runtime.
packages/spec is the one contract between metadata…"
; and the maintainer's 2026-08-27 ruling on staged
retirement (verbatim: 「项目在创业阶段,用户也很少,短期不考虑渐进。」).

Downstream


维护者速读

这是什么事 —— 我们的开源包 @objectstack/spec 里,放着一批只有云端在用的类型:环境、租户、后台管理的数据结构,2087 行。它们的生产者和消费者都在云端仓库,只有声明留在开源包里。你说的「cloud 的协议应该放在云端」,实测站得住 —— 开源那边对这批东西的真实依赖只有一个类型,其余全是注释和测试。

为什么值得现在处理 —— 不是因为坏了。客户今天不受影响,SDK 那 15 个方法本来就没类型、而且是故意没类型(代码里有你 2026-08-29 的裁决记录)。值得处理是因为:只要这批类型还挂在开源包上,就会一直有人想把它们和方法接起来,而那样做编译通过、运行时读到空值。搬走之后,这个类型在开源包里根本不存在,误接在结构上就不可能了。同时它还顺手解决了 #12036 那个字段命名之争 —— 那个争议之所以要你拍板,唯一原因就是声明和实现分在两个仓。

选项 —— A:整个 cloud 子路径全搬走(我不推荐:会连"包格式"一起搬走,而那是包作者发包要用的,属于开源侧的东西)。B:按归属切开 —— 云端私有的 2087 行搬去云端,包格式那 1400 行留在开源包里但换个不叫 cloud 的名字。C:不动 —— 回到 #12036 的字段命名三选一。

我的意见 —— 推荐 B。但要如实说两点:一,这不是急事,客户今天不坏,你完全可以先放着;二,B 要花一次跨三个仓的协调(云端、objectui、开源仓各一张卡,还有先后顺序),不是一次改动能完成的。另外,删掉一个已发布的公开子路径算破坏性变更,要升 major 写发行说明 —— 这个我们 v17 对同一批 API 做过一次,当时就是不留别名直接换。

你要做的 —— 选一个:A / B / C

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions