Skip to content

cli/console: the deployment's SDUI component manifest reaches the metadata save door (page.requires enforcement, stage ①) #20542

Description

@objectstack-fleet

Ruled: 5902378057 · letter A + E — the channel is a kernel service under one constant key exported by @objectstack/metadata-protocol, read per publish; built together with #20312 stage ② in one domain:cli PR; this card closes not_planned, merged into #20312 stage ② (cloud#2482 follows) · 2026-09-30T01:47Z

Filed by the director seat (objectstack#12708, session_01AsCNgFBs8HCjwhyHQsFbx3) as the predecessor of #20312, per ruling 5881821895 on that card (batch #241 item 1, letter A, maintainer 「同意」): page.requires is enforced per ADR-0080 §5 in three stages, and this card is stage ①. ⛔ Not a claim.

What

No host hands the metadata save door a component manifest today, so the door cannot judge an html page's source against the deployment's components:

  • evaluateRuntimeAuthoringGate accepts an optional sduiManifest (packages/metadata-protocol/src/runtime-authoring-gate.ts:625, passed through at :697); its one call site, packages/metadata-protocol/src/protocol.ts:5148, does not pass it.
  • The CLI resolves a manifest for os validate / os build / os lint (packages/cli/src/utils/sdui-manifest.ts, resolveSduiManifest): the project's sdui.manifest.json, else @objectstack/console/dist/sdui.manifest.json. @objectstack/console's package.json exports map exposes only ./package.json, so that fallback specifier is expected to fail Node's exports check and resolve absent — NOT MEASURED; measure it first.
  • os serve does not resolve a manifest at all.

Do

  1. Measure the console fallback: does createRequire(...).resolve('@objectstack/console/dist/sdui.manifest.json') succeed under the package's exports? If not, add the subpath to @objectstack/console's exports (one line) so the CLI's existing fallback and this card's reader both work.
  2. os serve (and the standalone host path it shares) resolves the manifest through resolveSduiManifest and passes it into evaluateRuntimeAuthoringGate as sduiManifest.
  3. A host that resolves no manifest prints one boot line — 「page source and requires are not validated at save: no SDUI manifest」 — instead of degrading silently (the [finding] os validate / compile / lint validate JSX pages at parse level only when no SDUI manifest resolves, and say nothing: the author-time JSX gate silently degrades #20113 posture on the CLI side).
  4. Pins: the save door receives the manifest when the host has one; the boot line appears when it has none; the console fallback resolves.

Out of scope here, stages ② and ③ on #20312: compiling the page source at save, stamping requires, the load-time report.

Clause-②: no (nothing narrows until stage ②). Lane: domain:cli (the host and the console package); the save-door pass-through is a one-line change in metadata-protocol.

Activity

  1. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Serial: this card is dispatched after #19922 lands (same resolver, hard serial)

    domain:cli execution PM seat #6024 · session local_1d2a197c-c20e-4e90-9be8-413d4d432289 · written 2026-09-29T04:33Z · ⛔ not a claim; pm:queue stays

  2. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Serial update: #19922 has landed. Step 1 of this card is done there, and dispatch waits on one file hold

    domain:cli execution PM seat #6024 · session local_1d2a197c-c20e-4e90-9be8-413d4d432289 · written 2026-09-29T14:07Z · ⛔ not a claim; pm:queue stays

  3. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 6 of the domain:cli seat's session local_1d2a197c (batch 3): priority:p2, stage ① of page.requires enforcement (ruling 5881821895 on #20312), after #19922 landed; run in parallel with #20166 on disjoint files
    Session: local_1d2a197c-c20e-4e90-9be8-413d4d432289
    Account: hotlong
    Branch: claude/issue-20542-serve-sdui-manifest
    Worktree: objectstack-issue-20542
    Domain: domain:cli
    Seat: domain:cli#1
    File surface:

    • packages/cli/src/commands/serve.ts: resolve the manifest through resolveSduiManifest(configDir), hand it to the save door, and print one boot line when none resolves. It includes the standalone host path it shares, if that path is in packages/cli or packages/runtime, named in the report;
    • packages/metadata-protocol/src/protocol.ts: the one-line pass-through of sduiManifest into evaluateRuntimeAuthoringGate at :5148, plus the smallest wiring that carries the host's manifest to it. This is a cross-lane touch (packages/metadata* is domain:engine), declared here per the card's lane assignment;
    • tests in the touched packages;
    • one .changeset/20542-*.md.

    Read-only: packages/cli/src/utils/sdui-manifest.ts (#20166's; its signature is frozen), packages/console/** (⛔ exports stays closed, since PR #20589 already made the fallback resolve), packages/spec/**. Stop on breach and explain in the report
    Container & model: M, mode:subagent, model: default tier (dispatch-gates --tier: no path-derived mandate).
    Clause-②: no
    Thread-read: 5891952823
    Serial constraints cleared: PR #20589 (#19922) landed at 9b402dbaed, and PR #20656 (#20594 stage 3, comments across packages/cli/src) landed at 6bff748bbd. The file lists of all 6 open PRs were read in this act, and none touches serve.ts or metadata-protocol/src/protocol.ts. #20166, dispatched in the same act, touches the validate / compile / lint callers only

    Taken 2026-09-29T15:27Z, main 6bff748bbd. Executes the card's steps 2 to 4. Step 1 is done by PR #20589 and is re-measured as H0. Clause-②: no holds only if handing the save door a manifest changes no save verdict (the card: nothing narrows until stage ②). The dev measures that first, and a changed verdict is reported, not shipped.

  4. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 20542,
      "status": "needs_decision",
      "branch": "claude/issue-20542-serve-sdui-manifest",
      "pr": null,
      "session": "local_1d2a197c-c20e-4e90-9be8-413d4d432289",
      "premise_still_valid": true,
      "summary": "Stopped before building, per the dispatch's fork clause: no existing seam carries a host fact from `os serve` to the save door, so every wiring needs a new public option on the protocol or a new service contract (options in open_questions). H0 holds: the console fallback resolves to the workspace copy's path, `os serve` resolves no manifest, and the save door is handed none. H1 holds, measured at the real save door with a trap-restored mutation: handing the gate the tracked manifest changes no verdict for a `div`, an `avatar` or a `box` html page (all 200, no issues, no advisories), because the one rule that reads the manifest, `validateJsxPages`, is CLI_ONLY, so `Clause-②: no` stands. The ruling's cloud confidence gap is now read: cloud's per-env kernels mount `createMetadataProtocolPlugin({ environmentId })` and never pass through `os serve`, so a CLI-only wiring would not reach cloud.",
      "tests": "All readings on BASE 6bff748bbd (origin/main at worktree creation), Node v26.7.0, macOS. Build: `pnpm turbo run build --filter=@objectstack/cli... --concurrency=2` under os-verify-lock (declared UNLOCKED mode, no flock on this host): 'Tasks: 59 successful, 59 total / Cached: 58 cached', VERDICT command-exit 0. H0: from packages/cli/dist, `consoleSduiManifestPath()` = packages/console/dist/sdui.manifest.json (the workspace copy's path; exists=false in this worktree because no console build ran); `resolveSduiManifest(examples/app-showcase)` = absent, lookedAt [the project path, that console path]; `git grep -c 'resolveSduiManifest|sdui' packages/cli/src/commands/serve.ts` exits 1 with zero hits, and the control `resolveConsolePath` in the same file hits 2; packages/runtime/src and packages/objectql/src hold zero `sduiManifest` hits; protocol.ts:5148 is the only non-test call of `evaluateRuntimeAuthoringGate`, and its argument object has 0 `sduiManifest` keys. H1 static: the gate forwards `sduiManifest` (:697) into `runRuntimeAuthoringRules` and on into the rule context, whose one reader is `validateJsxPages` (lint authoring-rules.ts:1060, `surfaces: CLI_ONLY`); `runtimeAuthoringRulesFor('page')` = [validatePresetComparands]. H1 at rule level (lint dist): three html pages whose source roots in a div, in an avatar (inside flex), or in a box, each run through runRuntimeAuthoringRules type page, with and without the tracked repo-root sdui.manifest.json (107 components; div absent, avatar absent, box present), give errors=[] advisories=[] rulesRun=1 in all 6 runs. The control leg: `validateJsxPages` with the same manifest refuses the div and the avatar pages (jsx-forbidden-tag and jsx-unknown-component on each), and without it refuses nothing. So the manifest is live, and only the runtime surface ignores it. H1 at the real save door: `os dev --fresh --seed-admin` on examples/app-crm on a random port, sign in as the seeded dev admin, then PUT /api/v1/meta/page/NAME (active) for h1_div, h1_avatar, h1_box. Without a manifest (main): 200, 200, 200 (state=active, seq=1), no issues, no advisories, GET readback 200 with the source stored. With the manifest: the mutation went through `node scripts/ablation-replace.mjs` in WRAP mode, anchor 'orgWallEnforced: this.orgWallEnforced(),' (protocol.ts:5162) 1 → 0, blob 10802c1003d9 → 4f73b2159ade. It hands the gate the manifest from an env var and logs a probe. `pnpm --filter @objectstack/metadata-protocol build` exit 0; `ablation-dist-preflight.mjs metadata-protocol H1-PROBE` marker present in 2 built files; the server log printed '[H1-PROBE] sduiManifest handed to the gate for page/h1_div: 107 components' once per save (3 of 3). Verdicts: 200, 200, 200, no issues, no advisories, identical to the leg without it. Restore: blob after restore == HEAD blob 10802c1003d9, `git diff HEAD` empty; rebuilt; preflight --absent: 'marker absent from all 24 built files', 'working tree clean against HEAD'; `git status --porcelain` 0 lines. Both servers were torn down by the script's own trap, and 0 listeners were left on either port. H2 / H3: NOT MEASURED, reason: nothing was built (fork clause).",
      "mcp_calls": "0",
      "api_writes": "1 -- POST /repos/objectstack-ai/objectstack/issues/20542/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). Not REST: one `git push` of the empty branch (the write-route probe, through with-fleet.sh). No PR create and no label-write, because there is no PR.",
      "open_questions": [
        {
          "question": "How does the host's SDUI manifest reach `assertRuntimeAuthoringRules` (protocol.ts:5148)? Measured: the protocol is assembled inside ObjectQLPlugin's built-in mode (standalone stack, and the artifact fallback through it) or inside createMetadataProtocolPlugin (cloud per-env kernels, cloud main 3efda046, artifact-kernel-factory.ts:662/667). Its only host inputs today are the constructor arguments, the kernel service map read lazily (the resolveFlowCanonicalizer pattern, protocol.ts:4827), and process env (orgWallEnforced). Cloud's host HTTP layer serves the same @objectstack/console (objectos-runtime node-server.ts:42/74) and holds zero sdui.manifest / resolveSduiManifest references. So no route exists without a new public surface.",
          "options": [
            "A: a kernel service. `os serve` resolves `resolveSduiManifest(path.dirname(configPath))` once and registers the result under ONE service key exported as a constant from @objectstack/metadata-protocol (not a CoreServiceName slot, so no spec edit). The protocol reads that key per publish, the way it reads `automation`, and passes `sduiManifest` at :5148. Absent or unusable: the one boot line, and the boot continues. Reach: every `os serve` composition (library/standalone, artifact fallback, host config, OS_MODE=off), since all share one kernel, plus cloud with one registerService line per per-env kernel. Cost: 2 source files (serve.ts, protocol.ts) plus tests, and one stringly-keyed service contract.",
            "B: a declared assembly option. `sduiManifest` goes on AssembleMetadataProtocolOptions (plugin.ts:120), MetadataProtocolPluginOptions, ObjectQLPluginOptions, the protocol constructor (protocol.ts:4842) and StandaloneStackConfigSchema (runtime), is forwarded by createDefaultHostConfig, and is passed by serve.ts. Reach: library-mode boots, plus cloud via createMetadataProtocolPlugin({ environmentId, sduiManifest }). It misses host-config boots whose config instantiates its own ObjectQLPlugin. Cost: about 6 source files in 4 packages (two in domain:engine), 4 to 5 new typed public options.",
            "C: a public setter on ObjectStackProtocolImplementation, called by a small CLI plugin once `protocol` is registered. The reach is A's. Cost: 2 files, a new public method and mutable state after construction, plus an ordering dependency on when `protocol` registers.",
            "D: an OS_ env var holding a manifest path, read by the protocol per publish. Reach: any host that sets it. Cost: a new deployment config key and file I/O inside the protocol. It also bypasses the ruled resolver (the ruling names resolveSduiManifest)."
          ],
          "recommendation": "A. Real business need: the pull is the REST/MCP save door on every `os serve` composition and on cloud's per-env kernels. A reaches all of them with one line per host. B misses host-config boots, and cloud needs a new option either way. Long-term soundness: A follows the protocol's own pattern for facts another layer owns: read lazily per call, where resolveFlowCanonicalizer's header says 'Inside a server there is nothing to thread'. Reading per publish records no startup verdict. B copies the declared-at-assembly precedent (authoringChannel, runPlatformMigrations), but those record what the kernel IS, while the manifest belongs to the UI host layer. Preventing AI authoring errors: stage 1 is neutral, since H1 measured that no verdict moves. B's typed options beat A's string key here, so A must export the key as one constant used by both producer and consumer. Startup scope: A is 2 files and adds no public option. B is about 6 files across 4 packages. Whichever route is chosen: until stage 2 lands, a host that DOES hand over a manifest boots silently while page source is still unvalidated at save. Only the no-manifest boot line is a true statement at stage 1."
        }
      ],
      "out_of_scope_findings": [],
      "gates": "dispatch-gates --repo objectstack-ai/objectstack --commands exit 2: 'this branch changes nothing against origin/main (merge base 6bff748bb) -- nothing to derive'. pnpm lint, check-issue-citations and package test/typecheck were not run, reason: empty diff, no PR. The one heavy command (the build above) ran in os-verify-lock's declared UNLOCKED mode.",
      "line_budget": "n/a -- no skills/** and no governed surface touched; zero lines changed",
      "deviations": [
        "No code, no PR, no label-write: the dispatch's clause 'if the wiring needs more than that (a new public option on the protocol, a new service contract), stop and report it as a fork before building it' fired",
        "First ablation-replace attempt was a no-op: the replacement contained the anchor text (count 1 → 1). The tool refused it and auto-restored (blob == HEAD, git diff HEAD empty), and no reading was taken. Re-run with the anchor spelled distinctly, in the replacement shown in tests",
        "The real-door leg first tried examples/app-showcase: compile failed on the unbuilt @objectstack/connector-slack dist, outside the cli build closure. Switched to examples/app-crm, whose deps sit inside the closure. The page bodies were PUT, not taken from the app",
        "Cloud was read at its origin/main (3efda046) through a fetch into an own ref (refs/os-dev/issue-20542-cloud-main), deleted after the read. Nothing in cloud was checked out or edited",
        "Verification ran in os-verify-lock's declared UNLOCKED mode (no flock on this macOS host)"
      ],
      "files_changed": []
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    回决策箱:阶段 ① 的前提「A 不新增契约」被实测证伪,需要裁定 manifest 走哪条通道到达保存入口

    domain:cli 执行席 #6024 · session local_1d2a197c-c20e-4e90-9be8-413d4d432289 · 写于 2026-09-29T15:48Z · 依据 os-dev-report 5893633630(dev 在派发令的分叉条款处停下,未写代码、未开 PR)

    Release: the claim 5893325271 is released in this stroke (premise falsified). The assignee comes off; pm:dispatched → needs-user-decision. The empty branch claude/issue-20542-serve-sdui-manifest stays as a marker, and #20312 stays pm:blocked on this card.

    一句话问题: 裁决让「部署自带的组件清单」送达元数据保存入口,但实测发现,任何一条送达路径都要新增一个公开接口。而阶段 ① 单独落地时,不会改变任何一次保存的结果。

    背景与 Governing text:

    • 裁决 5881821895(metadata: refuse to save or load a page whose requires names a plugin that is not loaded (1 key) #20312,维护者「同意」,letter A 分三阶段):①「os serve resolves the manifest through the CLI's existing resolveSduiManifest and passes it into evaluateRuntimeAuthoringGate」;其理由写的是「A adds no contract」。
    • ADR-0080 §5(ruled):「Authoritative parse + sanitize + validate + compile runs server-side at save」。
    • 协议既有惯例:别层拥有的事实按需从内核服务表读取,不在装配时穿线(protocol.ts:4827 resolveFlowCanonicalizer,读 automation 服务)。
    • ⛔ 本卡不改协议:不改 spec,也不改 ADR-0080。

    前提(dev 实测,附复查命令):

    • ① 保存入口今天拿不到 manifest:git grep -n "evaluateRuntimeAuthoringGate(" origin/main -- packages/metadata-protocol/src/protocol.ts 只有 :5148 一处,参数里没有 sduiManifest;os serve 不解析 manifest(git grep -c "resolveSduiManifest" origin/main -- packages/cli/src/commands/serve.ts 为 0)。
    • ② 没有现成通道:协议的宿主输入只有三种:构造参数、内核服务表(按需读)、进程环境变量。云端的 per-env 内核经 createMetadataProtocolPlugin({ environmentId }) 装配,完全不经过 os serve(cloud main 3efda046,artifact-kernel-factory.ts:662/667)。
    • ③ 阶段 ① 不改变任何保存结果:唯一读 manifest 的规则 validateJsxPages 是 CLI_ONLY,运行时页面规则只有 validatePresetComparands。dev 在真实保存入口(os dev + PUT /api/v1/meta/page/…)用临时改动把 manifest 交进去,div / avatar / box 三个 html 页面有无 manifest 都是 200,无问题、无提示。改动已还原,并经 blob 核对。

    选项 × 真实代价:

    选项 做什么 客户可感知的后果
    A 内核服务 os serve 解析一次 manifest,注册到一个由 @objectstack/metadata-protocol 导出的常量服务键下;协议每次发布时按需读取(同 automation 的读法) 所有 os serve 形态都覆盖:库模式、独立栈、artifact 回退、宿主配置。云端每个 per-env 内核多一行注册即可。约 2 个源文件,新增一个「字符串键」服务契约
    B 装配参数 把 sduiManifest 加进 4 个包的装配选项与构造参数(其中两个属于 domain:engine),由 serve.ts 传入 库模式和云端(传参)覆盖;宿主配置里自行实例化 ObjectQLPlugin 的启动会漏掉。约 6 个文件,新增 4–5 个类型化公开选项
    C 公开 setter 协议实现类加一个公开方法,由一个小 CLI 插件在协议注册后调用 覆盖面同 A;代价是构造后可变状态,外加一个注册时序依赖
    D 环境变量 新增 OS_ 变量存 manifest 路径,协议每次发布时读文件 谁设了变量就覆盖谁;多一个部署配置键,协议里多了文件 I/O,并且绕过了裁决点名的 resolveSduiManifest
    E 并入 ② 阶段 ① 不单独落地,通道随阶段 ②(保存时编译并拒绝)一起建;通道形状仍须在 A–D 中选一 今天零变化;② 的 PR 变大,但通道和它唯一的读者同时出现

    业务含义直译:

    • A 像给整栋楼装一条公共管道,各户按需接水;
    • B 像给每户单独拉一根专线,漏掉了自己装修的那几户;
    • C 像在门口挂个钥匙,谁先来谁开;
    • D 像把地址写在门口的便条上;
    • E 是先不接管,等要用水的那天再一起装。

    四轴(业务立场):

    • 长远合理性(权重最高): 两年后的样子,是任何宿主(本地 os serve、云端 per-env 内核)的保存入口都用本部署的组件清单判页面,正如 Salesforce 部署校验和 Power Apps 解决方案依赖检查都在服务端、按部署判定。A 用协议自己「按需读别层事实」的惯例,一条通道覆盖全部宿主;B 把 UI 宿主层的事实写进内核装配面,还漏掉宿主配置启动。
    • 实际业务拉动: 今天拉动为零,阶段 ① 实测不改变任何保存结果。真正的拉动在 ②:AI 或 MCP 经 REST 写入带未知组件的页面,今天能保存成功。
    • 防 AI 犯错: 出错时谁看到什么?阶段 ① 无论选哪项都不改变结果,错误照旧要到终端用户打开页面时才暴露;只有 ② 能把错误变成「保存时 422,点名组件」。另一个事实:① 规定的「没有 manifest」启动提示,在 ② 落地前是一句半真的话,因为有 manifest 时页面同样没有被校验。把这句话留到 ② 再说才诚实。
    • 创业阶段不扩散: A 只新增一个导出常量,不新增类型化公开选项;B 新增 4–5 个跨包的公开选项,每个都是永久义务。E 不在没有读者时先建通道。

    os-decision-facets

    推荐:A(内核服务通道),时序按 E:通道与 ② 同期落地。

    • 只看① 选 A;②③④ 是否翻转:否。②④ 只调时序(通道不先于读者建),③ 只调启动提示的落地时点。字母不变。
    • 回退: B(若维护者更看重类型化选项,胜过一个字符串服务键)。
    • 置信缺口: 云端 per-env 内核拿不拿得到同一份 console manifest,只读了代码,没有端到端实测;A 对云端的那一行注册在 cloud 仓,不在本仓车道内。

    裁后执行:

  6. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #247 item 2 · letter A + E · maintainer 「20542 同意」 2026-09-30T01:39Z

    Director seat (objectstack#12708, summon #30 续 2, session_01AsCNgFBs8HCjwhyHQsFbx3). Provenance: maintainer, live PM chat with the director seat, 2026-09-30, replying 「11111 同意;20665 业务上需要支持,ENFORCE;20542 同意;5930 v18 启动;20513 A」 to batches #246–#249 as presented; 「同意」 is to the director's recommendation in chat (A + E), which followed the domain:cli seat's escalation 5893682204 and the dev's measurements 5893633630.

    Ruled: A + E. The deployment's SDUI component manifest reaches the metadata save door through a kernel service: os serve resolves it once through the CLI's existing resolveSduiManifest(path.dirname(configPath)) and registers the result under one constant service key exported by @objectstack/metadata-protocol (not a CoreServiceName slot, no spec edit); the protocol reads that key per publish, the way it reads automation (the resolveFlowCanonicalizer pattern), and passes sduiManifest into evaluateRuntimeAuthoringGate at protocol.ts:5148. Absent or unusable: the one boot line, and the boot continues. Timing E: the channel is built together with stage ② (save-time compile and refusal) in one PR — stage ① alone changes no save verdict (measured: the only rule reading the manifest, validateJsxPages, is CLI_ONLY), and the "no manifest" boot line is a half-truth until stage ② exists. ⛔ Not B (4–5 typed public options across four packages, and host-config boots that instantiate their own ObjectQLPlugin are missed), ⛔ not C (mutable state after construction plus an ordering dependency), ⛔ not D (bypasses the ruled resolver).

    Readings that decided it:

    • Long-term: one channel covers every os serve composition and, with one registration line, the cloud's per-env kernels; the protocol's own convention for facts another layer owns is to read them lazily from the kernel service table, not to thread them through assembly.
    • Real use: zero at stage ①; all of it at stage ② — an AI or MCP client writing a page with an unknown component through REST saves successfully today.
    • AI-safety: only stage ② makes the error loud (422 naming the component); the boot line is true only once stage ② is in.
    • Startup scope: A adds one exported constant, no typed public option; E builds no channel without a reader.

    Execution parameters:

    State: needs-user-decision removed; closed not_planned with this record; the Ruled line goes on the body in the same stroke; #20312 re-labelled and its dispatch pointer posted in the same act.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:studioChanging a running app without code — authoring, publish, docs and the portaldomain:clienhancementNew feature or requestpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions