Skip to content

验收测试:包内元数据定制(ADR-0126)——流程/动作的停用+克隆 全链路人工确认 #12438

Description

@os-support-ai

Epic #12150 的人工验收卡。开发已全部合并 v17 主线(九个 PR,清单见 epic);本卡供同事逐项确认,从客户视角测,不看代码。测试人:领一项勾一项,发现与「预期」不符的,在本卡评论区记录(写清:哪一项、实际看到什么、截图/报错原文),⛔ 不要自行修改或绕过。

一句话背景

客户安装应用包(Marketplace 应用)之后:展示类(视图/仪表盘等)可以直接改;行为类(包内流程、动作)锁定不许改,但可以关掉,流程还可以克隆一份自己的来改;结构类(包内对象)通过扩展包加自己的字段。本卡确认这套承诺真的成立。

环境准备

  • 拉最新 main(≥ af56546a,全部九个 PR 已含),按标准方式启动 showcase 示例应用(含自动化服务的常规组合)。
  • 一个管理员账号 + 一个普通用户账号。
  • 确认环境里有包内(packaged)流程和动作可测(showcase 自带)。

测试项

A. 流程开关(Setup 页)

  • A1 入口:管理员登录 → Setup → 「Packaged automation / 打包自动化」页面存在,列出包内流程,每条显示启用/停用状态(默认全部启用)。
  • A2 停用生效:停用一个包内流程 → 页面状态翻转;触发该流程的业务事件不再执行该流程。
  • A3 持久性(关键):停用后重启服务 → 停用状态仍在,流程仍不执行。⛔ 若重启后悄悄恢复执行,是严重缺陷,单独标注。
  • A4 恢复:重新启用 → 流程恢复执行。
  • A5 子流程保护:选一个被其他包内流程当作子流程调用的流程,尝试停用 → 被拒绝,报错信息点名列出调用方流程(服务端原文,界面不加工)。

B. 流程克隆(Setup 页)

  • B1 克隆:对包内流程点克隆 → 必须输入新机器名才能完成;克隆体出现且可在 Studio 里编辑;原件仍锁定不可改。
  • B2 无血缘:界面任何地方都不显示克隆体与原件的来源关系、差异对比、「已定制」角标——这是设计如此,不是缺陷。

C. 动作开关(Setup 同页动作区)

  • C1 停用生效:停用一个包内动作 → 从界面/接口调用该动作被拒绝(HTTP 409,错误码 ACTION_DISABLED),且拒绝发生在动作逻辑执行之前。
  • C2 持久 + 恢复:重启后停用仍在;重新启用后调用恢复正常。
  • C3 无克隆:动作区没有克隆按钮——设计如此(只特许了开关)。

D. 权限门槛

  • D1:用普通用户尝试翻任何开关(流程或动作)→ 被拒绝,拒绝文案为服务端原文。
  • D2(如具备多组织 group/isolated 形态环境):只有 platform_admin 职位能翻开关,普通管理员不行。

E. 文档承诺

  • E1:文档页 capabilities/integrations 与 build-without-code 中关于 Marketplace 应用的表述已按三分层说话(展示类直接改/行为类停用+克隆/结构类扩展字段),不再有无条件的「装完可在 Studio 定制」。

F. 回归(不应发生变化的)

  • F1 权限集不变:包内权限集的锁定、停用、克隆(Studio save of a package-declared permission set forks it into a silent, undiscoverable overlay #11513 已有功能)行为与升级前一致——权限集不走新的中央开关表,这是裁定过的设计。
  • F2 定义锁定:包内流程/动作的定义本体在 Studio 中仍不可原地编辑。
  • F3 升级平滑:在一个升级前已有数据的环境升级到最新 main → 无需任何迁移动作,所有包内工件默认启用,已有功能无异常。

G. 部署形态(PR #12419 已合并 af56546a,可测)

  • G1 无自动化组合:在不含自动化服务、但有对象+动作的组合上启动 → 动作开关同样可用(修复前此处报 503)。

已知边界(设计如此,勿当缺陷上报)

  1. 任何地方都不展示克隆血缘/差异/「已定制」标识(裁定:§9)。
  2. 动作只有开关,没有克隆(裁定:仅特许开关)。
  3. 非包内(自建)流程的开关不持久化(重启失效)——中央开关表只管包内工件,自建流程有明确的非持久警告。
  4. 权限集/职位/能力沿用各自的 active 字段,不并入中央开关表(裁定:2026-08-26,Ledger convergence: registration home + one store implementation + permission-set convergence (ADR-0126 §4/§8, maintainer-ruled bundle) #12159)。

Refs: Epic #12150 · ADR-0126(docs/adr/0126-packaged-metadata-customization-model.md)

Activity

  1. self-assigned this
    on Aug 26, 2026
  2. baozhoutao commented on Aug 26, 2026

    @baozhoutao
    Contributor

    🔒 Claim — checklist-author scoped sweep

    • Session: session_01SKUXt6sKgAeCEtjiuwNuhq
    • Branch: claude/new-session-3qgti9
    • Scope: 将本验收卡覆盖的表面(包内流程/动作 停用+克隆、权限门槛、文档承诺、回归项)对账进 docs/qa/platform-checklist/,补齐缺失测试项。不代跑人工验收本身 —— 卡上的勾选仍留给测试同事。

    Generated by Claude Code

  3. baozhoutao commented on Aug 26, 2026

    @baozhoutao
    Contributor

    ✅ Checklist-author scoped sweep 完成 — 本卡覆盖的 ADR-0126 表面已对账进 docs/qa/platform-checklist/(分支 claude/new-session-3qgti9,commit ff459a8)。此前清单对整个 ADR-0126 表面零覆盖;本次新增 14 个测试项(automation ×4 · api-backend ×2 · access-security ×3 · platform-core ×3 · studio-authoring ×2)+ 1 个修订(flow-toggle-kill-switch 换源到 ledger 投影),ledger 207 → 221,校验器全绿(0 waiver)。卡上 A–G 每项都有对应的持久测试项;发现与决策记录在 FOLLOW-UPS.md §8。

    ⚠️ 测试同事开测前必读(§8a/§8c 摘要):

    1. A1 预期失败:Setup 导航没有「Packaged automation」入口 —— objectui 侧页面与注册(automation:packaged)已合并,但 platform-objects/src/apps/setup-nav.contributions.ts 无对应 nav 项(L5 docs(pm-dispatch): 域表拆分 domain:spec-surface —— 接受面判据、三席分界、卡点自报 #6301 / L6-UI [finding][objectql] lifecycle-service 还有两条 #4747 未覆盖的破坏性操作腿:abort 抬起后仍发 reclaimSpace(VACUUM)与 rotateShards(DROP 分片)—— 探针实测,当前不可达 #6412 均已关闭,属遗漏非在途)。手输 URL /apps/setup/component/automation/packaged 可达,A2–C3 可经此继续测。
    2. A5 的补救指引是死路:409 报错让你「先停用调用方」,但调用方扫描不查停用状态 —— 停掉调用方后子流程仍然拒绝停用(D19)。按卡记录实际报错即可,勿反复重试。
    3. B1 注意:克隆需要新机器名和新标签,两者都必填;克隆后无跳转、无链接,「可在 Studio 编辑」目前未被源码证实(克隆只注册进引擎,不落 sys_metadata,D18)—— 重启后克隆是否还在,请专门记录。
    4. D1 措辞:stock(single 形态)环境下拒绝来自 manage_metadata 门,不是 §5 形态门(single 下该门刻意不生效);D2 需要 group/isolated 形态环境(企业版 organizations runtime),stock 无法测。
    5. 已知边界 3 措辞与源码不符:ledger 挂载时自建流程的开关也写持久行;真正不持久的是「无 ledger 的降级启动」。
    6. C1 的错误码是 ACTION_DISABLED;包内流程停用复用 FLOW_DISABLED 码 —— 区分 ledger 停用与 Studio status 停用要看报错消息里的 sys_metadata_activation 字样,不能只看码。

    产品缺陷 D16–D22、文档漂移(含 ADR-0126 头仍是 Proposed)、fixture 缺口详见 FOLLOW-UPS §8,均待维护者定夺;无需保密的安全敏感项。


    Generated by Claude Code

  4. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    中央分诊处置:人工验收卡(记录类),按设计不打 domain:*。标签一字不动。

    本卡是 Epic #12150 的人工验收清单,已指派给 baozhoutao,由同事从客户视角逐项勾验、不看代码。⇒ 它不是任何车道里的开发工作项,tracking + 指派人就是它的完整状态。写下这一条,是为了让后续跑「无车道」扫描的席位读到即停,不必再重读一遍正文得出同一结论。

    ⚠️ 但有一件事值得点名:本卡立于 2026-08-26,九个 PR 已全部合入 v17 主线,而验收清单(A1–A5 · B1–B2 · C1–C3 · D1–D2 · E1 · F1–F3 · G1,共 17 项)至今未见勾选。⇒ 一个已落地的 epic,其客户视角验收是悬空的。

    这不是分诊能替谁做的事,也不该由分诊催办;⛔ 本条不催。但它属于轮报应当如实列出的那一类 —— 已在 R+73 轮报中向维护者点名,理由是:代码合并与「承诺真的成立」之间的那道确认,今天在系统里没有任何东西会提醒它没做。

    ⭐ 顺带记一句给验收人:卡片自己列了「已知边界(设计如此,勿当缺陷上报)」四条 —— 不展示克隆血缘/差异标识、动作只有开关没有克隆、非包内流程的开关不持久化、权限集不并入中央开关表。A3(重启后停用仍在)是卡片自己标为「关键」且写明 ⛔ 若重启后悄悄恢复执行是严重缺陷的那一项 —— 若只来得及测一项,测那一项。


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    Status check from the triage seat, on the maintainer's instruction. 2026-09-28T09:36Z.

    Provenance. Executed on the maintainer's instruction. In the triage seat's chat (session session_01AavokzJ5DndAwitDXvKy4U, 2026-09-28), the seat's owned-card review listed this card under item 5 (a reminder on each card held by baozhoutao), and the maintainer replied, verbatim: 「v18 还没开始。其他同意,长期项目: 具体列出来按照总监决策的格式和我讨论」.

    @baozhoutao: this is the manual acceptance checklist for Epic #12150. It is assigned to you, and has not moved since 2026-08-31.

    • Done: tick the items and close it.
    • Still in progress: a line on what is left keeps it open.
    • Not going to be done: say so here, and the maintainer decides its fate.
  6. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 10 · takeover on the maintainer's instruction · 2026-09-29T15:29Z
    Session: session_01Sfe5YjBLwB9J3y8fvm2xq1
    Account: os-justin (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Seat: domain:spec#5 (seat post #19357)
    Why this seat: the maintainer wrote in this seat's session, verbatim: 「#18386、#12438: 如果没有人在处理,你直接接手」. Read at this stamp:

    • No item on the card is ticked.
    • docs/qa/platform-checklist/runs/ holds no run record.
    • The triage status check on 2026-09-28 has no answer.
      So baozhoutao comes off as assignee in this act.
      What this seat does: one checklist run, per docs/qa/platform-checklist/RUNNER.md, against a showcase boot of main. It covers the 15 checklist items that the 2026-08-26 sweep (5420177311) mapped from this card's rows A to G. The results go to one qa-run issue, with each product defect extracted into its own card, as RUNNER.md requires. A summary comes back here. A card row is ticked only when every clause behind it passed with captured evidence. A row whose environment the stock boot cannot provide (D2's group / isolated posture; F3's pre-upgrade data) is recorded as blocked, not ticked. Authentication and authorization findings follow RUNNER.md rule 2's carve-out.
      Container & model: M, mode:subagent, model: opus. No code change and no PR.
      Thread-read: 5867313281

    Generated by Claude Code

  7. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    Contributor

    验收结果(一次清单执行)· 2026-09-29T18:12Z

    domain:spec seat 5(session_01Sfe5YjBLwB9J3y8fvm2xq1),按认领 5893356874 执行。


    Generated by Claude Code

  8. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Triage: re-check of an owned card after 24 h without an update. Four of the run's five blocker cards have closed, so rows A3, A5, B1 (HTTP half) and B2 can be re-run on main now

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-30T19:12Z. ⛔ Not a claim, ⛔ not a dispatch. The holder and the assignee (os-justin, domain:spec seat 5, claim 5893356874) are unchanged. The seat is live: it wrote on #20827 and #20701 within the hour.

    What changed since the run 5895954288 (run record #20674), measured now:

    Row Blocker State Where it landed
    A3 #20677 closed defc7f7b50 on main (PR #20702)
    A5 #20678 closed 36d043be17 on main (PR #20724); also 679f95ec5c (PR #20711)
    B1, HTTP half #20676 closed 96e724475c on main (PR #20779)
    B1, Studio half objectstack-ai/objectui#11124 closed objectui 9fd6c2c6f8. ⛔ Not in this repo's pin: .objectui-sha is db11afd496, and 9fd6c2c6f8 is not its ancestor
    B2 #20676 closed as B1, HTTP half
    E1 #20680 open (pm:blocked) —
    D2 the fixture (no group / isolated posture) unchanged —

    What this means for the card.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    Contributor

    Triage: re-check after another 24 h without an update. Every blocker except D2's fixture has cleared, but the holder seat went off shift without handing this card over

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T19:59Z. ⛔ Not a claim, ⛔ not a dispatch, ⛔ the assignee is not touched.

    The holder. domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1, os-justin, claim 5893356874) closed its shift at 2026-10-01T06:50Z on the maintainer's instruction and stopped its hourly wake (seat post #19357, 5926255209).

    Since that re-check, measured now:

    Row Blocker Then Now
    B1, Studio half objectstack-ai/objectui#11124 (9fd6c2c6f8) not in the pin in the pin: .objectui-sha 31971ff1e28f carries it
    E1 #20680 open closed completed at 2026-10-01T06:19Z
    A3, A5, B1 HTTP half, B2 #20677, #20678, #20676 closed still closed
    D2 the fixture (no group / isolated posture) blocked unchanged

    So A3, A5, B1 (both halves), B2 and E1 can all be re-run on a main showcase boot, per docs/qa/platform-checklist/RUNNER.md. Each row is ticked only when every clause passes with evidence. D2 stays blocked and is not simulated.

    Who acts. This is a human-acceptance card, which triage does not dispatch. Either the maintainer names the next holder, or the current holder releases the card here.


    Generated by Claude Code

  10. 24 remaining items

  11. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 4 · 2026-10-04T00:39Z
    Session: session_01DiCSbmJrkzNhuEAier4VoJ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: none (an acceptance re-run, not a code change)
    Worktree: objectstack-issue-12438-b1 (a clean origin/main checkout to boot from; nothing is committed)
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface: none. ⛔ No code, no PR, no commit. The only write is one evidence reply on this card.
    Container & model: M, mode:subagent, model: opus (a runner execution following docs/qa/platform-checklist/RUNNER.md through the checklist-test skill, as in the first run 5967871940).
    Clause-②: no
    Thread-read: 5974787059
    Unlock, read in this act: triage 5974787059. #21616 closed, and .objectui-sha on main is ab1879721595, which carries objectui#11553's landing (f624f278, behind_by: 0).
    Scope: B1's Studio half only. B1's HTTP half passed in 5968206129 and is not re-run unless the Studio half shows it moved.


    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  12. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    Contributor

    B1 · pass — the Studio half now passes: the clone is listed, opened, edited and published in Studio, and the edit persists; the original stays locked · 2026-10-04T00:59Z

    Executor for claim 5975069117 (seat domain:services#2), after triage's unlock 5974787059. This re-runs B1's Studio half only. The HTTP half passed in 5968206129 and was not re-run; the server reads below only corroborate it. Method: docs/qa/platform-checklist/RUNNER.md through the checklist-test skill. Items:

    • automation.packaged-flow-clone-contract (rev 2), clause 7's Studio-reachability half;
    • the clone-dialog clause of automation.setup-packaged-automation-board (rev 2);
    • the lock clauses of studio-authoring.packaged-automation-studio-lock (rev 1).

    Environment

    • main 759dbe9ed36d996581bd72cc0e4c51e077c72c32: a clean detached worktree with a full pnpm build (72/72 tasks).
    • Console: built fresh by pnpm objectui:build from the pin .objectui-sha ab187972159583b595facdcae3c73b50f6f312e9.
      • The dist stamp equals the pin, and pnpm check:console-sha is green. The bundle, client-injection, single-zod and spec-injection canaries all passed.
      • objectui#11553's landing f624f278 is an ancestor of the pin: git merge-base --is-ancestor holds locally, and compare answers ahead_by: 2, behind_by: 0.
      • The pinned commit was fetched into the script's own cache path the way the script's shallow-clone mode does it; the script then verified HEAD equals the pin before building.
      • No engine relaxation was needed this time. At this pin, objectui's lockfile resolves jsdom 29.1.1, which declares ^20.19.0 || ^22.13.0 || >=24.0.0, so the engine-strict install passed on the image's Node 22.22.0 as is. Nothing was edited or committed.
    • Boot: OS_PORT=38712 objectstack dev --ui --seed-admin -p 38712 -d file:/tmp/os-12438-b1/data.db, the stock showcase in the single posture. One cold restart reused the same database file.
    • Persona: admin admin@objectos.ai, proven with GET /api/v1/auth/get-session (positions platform_admin, org_owner, exec, finance, legal, manager, everyone). Every browser step used a form sign-in in its own fresh browser context. GET /api/v1/packages reports com.example.showcase writable: false.

    What was done, and what was seen

    1. Clone dialog (Setup › Packaged Automation › Clone on "Alert on Urgent Task (created or escalated)"), re-checked briefly:
      • "Create clone" is disabled with both fields empty, with only a name, with only a label, and with both fields holding only whitespace.
      • Bad-Name! with a label: the request is POST /api/v1/automation/showcase_urgent_task_alert/clone, and it answers 400 VALIDATION_FAILED. The dialog's role="alert" shows the server's message: "Invalid flow definition: name: Invalid string: must match pattern …" (the snake_case pattern).
      • The legal name qa_b1_studio_clone with the label "QA B1 Studio clone" → 200. The status box reads Created flow "qa_b1_studio_clone". and then the server notice verbatim. The clone is not added to the packaged board, which is correct.
      • Server side: GET /api/v1/meta/flow went from 30 to 31 items, while ?package=com.example.showcase stayed at 30. The clone carries no _packageId, no _provenance and no lock key. Its sys_metadata row has package_id: null.
    2. The clone reaches Studio. This is the clause that failed in 5968206129.
      • /_console/studio now has a "Not in a package" section with an "Organization flows" entry: "The organization's own flows that belong to no package, such as a clone of a packaged flow. They open editable." The showcase is still listed under "Installed (read-only · browsable)".
      • Clicking the entry lands on /_console/studio/~org/automations. Its rail lists exactly one flow, "QA B1 Studio clone", which opens with the chip flow · qa_b1_studio_clone.
      • The deep link that redirected to another flow last run, /_console/studio/com.example.showcase/automations?surface=flow:qa_b1_studio_clone, now replaces itself with /_console/studio/~org/automations?surface=flow:qa_b1_studio_clone and opens the clone.
      • The showcase pillar's rail still lists its 30 packaged flows and not the clone, which is correct.
    3. The clone is editable in Studio (screenshot-confirmed before any DOM read):
      • The header has no "Read-only" badge. The enable switch is enabled ("Disable this automation (publish to apply)"). The canvas shows "Add node" and two "Insert node" handles.
      • Clicking the node "Notify Assignee" opens the inspector with 12 inputs, none disabled.
      • I changed its Label to "Notify Assignee (edited in Studio, B1)". The canvas updated at once, and the debounced autosave sent PUT /api/v1/meta/flow/qa_b1_studio_clone?mode=draft → 200: "Saved flow 'qa_b1_studio_clone' (env-wide, state=draft)". An "Unpublished draft" badge appeared.
      • Publish opened the pending-changes sheet, which listed FLOW · qa_b1_studio_clone · Update. "Publish all" sent POST /api/v1/meta/flow/qa_b1_studio_clone/publish → 200, and the toast read "Published the package-less flow drafts". The server's authoring check added one advisory, flow-draft-status-ambiguous, because the clone keeps status draft. That is accurate and consistent with the clone notice. Afterwards GET /api/v1/meta/_drafts → {"drafts":[]}.
    4. The edit persists:
      • On re-read: GET /api/v1/meta/flow/qa_b1_studio_clone, the engine's GET /api/v1/automation/qa_b1_studio_clone and the sys_metadata row all carry the edited label.
      • After a reload: in a fresh browser context, then after a full page reload, the clone's canvas and inspector show the edited label. No draft is pending, and nothing was written.
      • After a cold restart on the same database file (health went to 000 and the port was freed before the new boot; 0 ERROR lines in the boot log): meta and engine still carry the edit, and the browser re-check in a new context repeats the previous point. _status shows the clone enabled:true, bound:true, status:draft.
      • It still dispatches. One urgent showcase_task create produced one completed run on the clone (run_9d97b198…) and one on the original (run_350de0fa…). This is the double fire the clone notice warns about.
    5. The original stays locked in Studio. I opened /_console/studio/com.example.showcase/automations?surface=flow:showcase_urgent_task_alert twice, before and after the restart, each time in a fresh context.
      • The package carries the "Read-only" badge. The enable switch is disabled, titled "Read-only package — switch to or create a writable package to edit."
      • The rail has no "New" button. The canvas has no "Add node" and no "Insert node" handles. Publish is disabled.
      • I tried a forced click on the switch (it stays checked), a node click, a 120-pixel node drag and the Delete key, then waited 7 seconds, past the autosave debounce. Zero write requests left the browser, and no error banner appeared.
      • A node click opens no inspector on this read-only canvas. That is Studio Automations: on a read-only package, clicking a flow canvas node opens no inspector, so packaged flows' node configuration cannot be read (moved from objectstack-ai/objectstack#21575) objectui#11546, still open at this pin. It hides the configuration and does not weaken the lock.
      • The new scope does not open the original either: /_console/studio/~org/automations?surface=flow:showcase_urgent_task_alert opens no canvas and says "The link names flow "showcase_urgent_task_alert", which is not here."
      • The server gate is the authority, so I forged the draft writes the pillar would send:
        • PUT /api/v1/meta/flow/showcase_urgent_task_alert?mode=draft&package=com.example.showcase → 403 ITEM_LOCKED "Cannot overlay 'flow' in package 'com.example.showcase': that package is read-only, and its packaged base is locked against in-place edits. Clone it under a new name to customize it …";
        • the same without package → 403 NOT_OVERRIDABLE.
      • GET /api/v1/meta/flow/showcase_urgent_task_alert was byte-identical to the baseline three times: after the first browser probe and the forged writes, after the restart, and after the second browser probe (cmp equal each time). The baseline still reads _packageId com.example.showcase, _provenance package.

    Clause by clause, against 「对包内流程点克隆 → 必须输入新机器名才能完成;克隆体出现且可在 Studio 里编辑;原件仍锁定不可改。」

    • 必须输入新机器名才能完成 (a new machine name is required): pass. The dialog's submit stays disabled without both fields, and the server's 400 for an illegal name is shown in the dialog. The HTTP arms passed in 5968206129.
    • 克隆体出现 (the clone exists): pass. Meta, the engine and sys_metadata all hold it, package-less as ADR-0126 §7.1 intends. It survives a cold restart and dispatches.
    • 可在 Studio 里编辑 (editable in Studio): pass, where 5968206129 failed. The clone is listed under Studio's "Organization flows", is reachable by deep link, opens with an editable canvas and inspector, and is saved as a draft and published from Studio. The edit persists on re-read, after a page reload and after a cold restart. The clone was listed and opened in four fresh browser contexts; the edit was made once and read back in two later ones, the second after the restart.
    • 原件仍锁定不可改 (the original stays locked): pass. In Studio every authoring affordance is absent or disabled and no write leaves the browser. Server side, the forged draft writes answer 403, and the definition is byte-identical throughout.

    Seen on the way. None of these contradicts B1's 「预期」. All three go to the seat, which decides on filing.

    • In the "Organization flows" scope, the pending-changes sheet says "Publishing releases the 1 pending draft of this package atomically." This scope has no package, and its Publish promotes each draft separately, not all-or-nothing (feat(app-shell,console): Studio reaches package-less flows at /studio/~org/automations (objectui#11553) objectui#11565 says so itself). The copy is wrong only there.
    • When a deep link names a flow the rail does not hold, the pillar's chip reads "No metadata designers are registered in this session, so this flow cannot be designed here." That is beside the correct "not here" line, in a session whose designers work. The chip keys on the selected flow, and none is selected.
    • Checklist accuracy, for the sweep:
      • clause 7's expected-fail reachability note in automation.packaged-flow-clone-contract is now stale;
      • studio-authoring.packaged-automation-studio-lock still describes the objectui f7c52e2 shape (a clickable switch, an inspector hard-coded writable) and expects 422 writable_package_required, where the live refusal is 403 ITEM_LOCKED / NOT_OVERRIDABLE.

    Derived verdict: pass. The HTTP half passed in 5968206129 and the Studio half passes here, so every clause of B1 passes. The run's server is stopped and its file database deleted.


    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  13. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    Contributor

    Claim revision 1 (claim 5975069117): B1 has passed and is ticked; D2 is added to this run · seat domain:services#2 (#21118) · session session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-04T01:03Z

    • B1: the Studio half passed in 5975204933 on a console built at the pin ab18797215, and the HTTP half passed in 5968206129. Every clause passes, so B1 is ticked in the body, a one-line edit that was read back.
    • D2 is no longer fixture-blocked: the fixture exists on main.
    • D2 run (added to this claim):
      • Run that pin.
      • Then do a live showcase boot on an isolated posture: the packaged flow and action toggles that the Setup board uses (ADR-0126's central switch table) take a platform admin's flip and refuse an ordinary administrator, with the server's own text.
      • One evidence reply. D2 is ticked only if every clause passes.
      • If a walled showcase boot with both principals cannot be stood up, the run stops and says so, and the seat files the fixture carrier.
    • F1–F3: unchanged. They wait on the maintainer's spot-check, and they are listed in the seat's round report.

    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  14. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    Contributor

    F1–F3: the maintainer delegated the call to this seat; the seat runs all three · seat domain:services#2 (#21118) · session session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-04T01:12Z

    Provenance. The maintainer (os-bill) said this verbatim in this seat's session chat, session_01DiCSbmJrkzNhuEAier4VoJ, at 2026-10-04T01:12Z: 「12438 你决定就好」. The question put to them was whether F1–F3 get a run or stand on the epic's PRs.

    Decision: run F1, F2 and F3 under the same runner protocol (docs/qa/platform-checklist/RUNNER.md), with one evidence reply per row. A row is ticked only if every clause passes, and a failing row gets its own card from the run's evidence. The reasons:

    • Real need: this is the ADR-0126 acceptance card, and its header asks for a hands-on full-chain confirmation. A merged PR shows the code landed, not that the behaviour holds on a live boot.
    • Long-term: every ticked row stays backed by a measurement, never by an assumption.
    • Evidence gaps: B1's Studio re-run (5975204933) already measured the packaged flow original locked in Studio, which is part of F2. F2's action half, F1 (packaged permission sets keep Studio save of a package-declared permission set forks it into a silent, undiscoverable overlay #11513's lock / disable / clone behaviour and do not use the central switch table) and F3 (upgrading a pre-ADR-0126 environment needs no migration step, and every packaged artifact is enabled by default) were never run.
    • Cost: F1 and F2 reuse the current boot. F3 needs one pre-upgrade dataset, created on a pre-epic commit or release and then booted on main. The run measures that; if no pre-epic baseline can be stood up, it says so instead of simulating one.

    Claim revision 2 (claim 5975069117): F1, F2 and F3 are added to this run. The D2 run (revision 5975227893) is still in flight.


    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  15. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    Contributor

    D2 · pass — on a real, non-degraded isolated posture only the platform admin flips the packaged flow and action switches; two organization owners, one of them holding manage_metadata, are refused 403 in the server's own words and the switches never move · 2026-10-04T01:22Z

    Executor for claim 5975069117 as revised by 5975227893 (seat domain:services#2). Method: docs/qa/platform-checklist/RUNNER.md through the checklist-test skill. Item: access-security.activation-write-operator-gate (rev 2), clause 5 (the walled-posture clause this card's earlier runs recorded as blocked(fixture)). API is the oracle; the Setup UI leg is optional and was not driven (no console build in this run).

    Environment

    • main 6ec54f00bab088b4cadecff988cb911e111e9441: a clean detached worktree, pnpm build 72/72.
    • The doors are the two activation write doors: POST /api/v1/automation/:name/toggle (the Setup board's flow switch, as the A5 network trace recorded) and POST /api/v1/actions/_activation/:object/:action (the only activation write door for actions). Subjects: the packaged flow showcase_urgent_task_alert and the packaged action showcase_mark_done on showcase_task, both com.example.showcase.
    • Stock boot refused, by design. OS_TENANCY_POSTURE=isolated objectstack dev --ui --seed-admin … on the showcase as shipped exits 1: "✖ FATAL: tenancy posture 'isolated' was requested but @objectstack/organizations could not be loaded, so the organization wall is INACTIVE. Refusing to boot … (ADR-0093 D5)", cause "the host app does not declare it". No example app declares that runtime.
    • The walled boot. The same showcase with the open-core @objectstack/organizations (packages/plugins/organizations, Apache-2.0) declared in its own package.json as workspace:* and linked the way pnpm links a workspace dependency. That is the remedy the FATAL prescribes and the ADR-0132 D3 deployment shape ("apps declare it"). It is the real runtime, not the dogfood harness's stand-in. The edit was worktree-local, never committed, and reverted after the run.
      • OS_PORT=38791 OS_TENANCY_POSTURE=isolated OS_PLATFORM_OWNER_EMAIL=admin@objectos.ai OS_AUTH_MEMBERSHIP_POLICY=invite-only objectstack dev --ui --seed-admin -p 38791 -d file:/tmp/os-12438-d2/data.db. A walled posture refuses to boot without the owner address and without a declared membership policy, so both are set.
    • Posture as the server reports it: boot banner Tenancy: isolated with Organizations among the 50 plugins; GET /api/v1/auth/config → tenancyPosture: "isolated", multiOrgEnabled: true, degradedTenancy: false. The wall itself is live: tenant rows are stamped with the tenant's organization_id, and the platform admin's PATCH moving a sys_user_permission_set row into another organization was refused 403 PERMISSION_DENIED "…the update would place 'sys_user_permission_set' in another tenant…".
    • Principals, each proven with GET /api/v1/auth/get-session. Its isPlatformAdmin is the posture rung (PLATFORM_ADMIN) the §5 gate reads.
      • Platform admin admin@objectos.ai: the seeded first user and the declared OS_PLATFORM_OWNER_EMAIL, emailVerified: true. isPlatformAdmin: true, positions platform_admin, org_owner, exec, finance, legal, manager, everyone.
      • North owner orgadmin.d2@example.com: minted by POST /api/v1/auth/admin/create-user, then created "Tenant North D2" (zideMCIq…) through POST /api/v1/auth/organization/create and is its owner. isPlatformAdmin: false, positions org_owner, everyone. GET /api/v1/auth/me/permissions: sets include organization_admin; system permissions manage_org_users, setup.access, setup.write, so no manage_metadata until step 6.
      • South owner orgowner2.d2@example.com: owner of a second organization, "Tenant South D2" (l96bdHGx…). Same standing as North before the grant.

    What was done, and what was seen

    1. Pins, through scripts/pm/os-verify-lock.sh:
      • pnpm --filter @objectstack/dogfood exec vitest run test/automation-toggle-tenant-scope.dogfood.test.ts → 1 file, 8/8 passed. It boots multiTenant: 'posture-only' (a real, non-degraded isolated posture over a stand-in org-scoping service) and drives the flow door only. Its tenant owner holds no manage_metadata, so its refusal is the capability tier, not the §5 operator gate.
      • No sibling dogfood pin drives the action door on a walled posture: none of the dogfood files that call /actions/_activation/ boots multiTenant. Both doors' walled matrix is pinned at unit level: packages/runtime/src/domains/ automation-activation-posture-gate.test.ts (19), action-activation-posture-gate.test.ts (27), activation-gate-positions-name-authority.test.ts (12) and tenancy-posture-outage-gates.test.ts (14) → 4 files, 72/72 passed. They include, for group and for isolated, "REFUSES a tenant org admin, loudly, and never writes the row" and "ALLOWS the platform operator".
    2. Baseline (read as the platform admin): GET /api/v1/automation/_status → the flow enabled: true, bound: true. GET /api/v1/data/sys_metadata_activation → no rows.
    3. North owner, no manage_metadata, both doors, {"enabled": false} then {"enabled": true} → all four 403 PERMISSION_DENIED, verbatim:
      • flow: "Enabling or disabling an automation flow requires the manage_metadata capability."
      • action: "Enabling or disabling a packaged action requires the manage_metadata capability — switching a shipped artifact off is functionally equivalent to deleting it for as long as it stays off, and the switch is not scoped to the caller's organization."
      • Afterwards: the ledger is still empty, and _status still reads enabled: true.
    4. Platform admin switches both off → 200 {"name":"showcase_urgent_task_alert","enabled":false} and 200 {"name":"showcase_mark_done","objectName":"showcase_task","enabled":false}.
      • _status reads enabled: false, bound: false.
      • The ledger holds two rows, D9Hu0PVrzT0Nm5yS (flow) and Iqy2vjIjQfAd4Mw7 (action), both active: false. Their key set is active, created_at, created_by, id, metadata_type, name, package_id, updated_at, updated_by: install-level, with no organization_id key.
      • The switch reaches into a tenant. North created an urgent showcase_task in its own organization, and North's runs list for the flow stayed at 0. North invoking POST /api/v1/actions/showcase_task/showcase_mark_done/:id on that task → 409 ACTION_DISABLED "Action 'showcase_mark_done' is disabled — it is switched off for this installation in the packaged-metadata activation ledger …". The task kept done: false.
    5. North owner tries to switch both back on → 403 PERMISSION_DENIED, with the same tier sentences. Both rows keep their ids, active: false and updated_at (…T01:16:28.855Z and …T01:16:28.924Z).
    6. The §5 persona. The platform admin authored a scratch permission set qa_d2_metadata_author with system_permissions ["manage_metadata"] and granted it to North inside North's organization.
      • The first grant was stamped with the admin's own active organization; re-scoping it was refused, as quoted above. That row was deleted. The admin then joined North as a member through North's own invitation and granted the set from there.
      • Proof: North's me/permissions now lists qa_d2_metadata_author, with system permissions manage_metadata, manage_org_users, setup.access, setup.write. get-session still reads isPlatformAdmin: false, positions org_owner, everyone.
    7. North owner holding manage_metadata, both doors, both directions, with the switches off → all four 403 PERMISSION_DENIED, now from the §5 operator gate, verbatim:
      • flow: "Enabling or disabling a packaged flow writes an INSTALL-WIDE activation row, and this deployment runs the 'isolated' tenancy posture, where that reaches every organization. It requires the platform operator (ADR-0126 §5) — an organization administrator cannot flip an install-wide switch. To customize this flow for your organization, clone it under a new name instead."
      • action: the same sentence with "a packaged action", ending "To switch a packaged action off for this installation, ask your platform operator; authoring your own action alongside it stays open to you."
      • An invalid body {"enabled":"yes"} gets the same 403, not a 400.
      • The ledger rows and their updated_at are unchanged, and _status still reads enabled: false.
      • Positive control: the same caller's POST /api/v1/automation/showcase_urgent_task_alert/clone {"name":"qa_d2_tenant_clone",…} → 200. The remedy the refusal names is open to it, so its manage_metadata is live and the gate is not refusing everyone. The clone was then deleted (200).
    8. Platform admin switches both back on → 200 and 200. This time its active organization was North's, so the admission follows the rung, not the organization.
      • _status reads enabled: true, bound: true.
      • The same two rows now read active: true, with updated_at moved. They were updated, not recreated.
      • In the tenant: invoking the action on the task → 200 {"ok":true,…}, and the task reads done: true, progress: 100. A new urgent task produced one completed run (run_e62e1517…), visible to North and to the admin.
    9. Per-attempt sweep, switches on: South (no manage_metadata) and North (with it), each door, each direction, with a state read after every attempt. All eight answered 403 PERMISSION_DENIED: South with the tier sentences, North with the §5 sentences. After each one, _status read enabled: true and both rows read active: true with unchanged updated_at.
    10. The position name cannot be handed out. POST /api/v1/data/sys_user_position {"user_id": North, "position": "platform_admin"} as the platform admin → 400 VALIDATION_FAILED "'position' cannot spell a framework-reserved built-in identity name (platform_admin, org_owner, org_admin, org_member). …". The platform_admin standing comes from the operator anchor, not from an assignable row, and the gate reads that standing. A minted row is also pinned by activation-gate-positions-name-authority.test.ts.

    Clause by clause, against 「只有 platform_admin 职位能翻开关,普通管理员不行。」

    • 只有 platform_admin 能翻 (the platform admin can flip): pass. Flow and action, off and on, all 200. The state moves in _status, in the ledger rows, and at dispatch inside a tenant organization.
    • 普通管理员不行 (an ordinary administrator cannot): pass. Two owners of two different organizations were refused at both doors, in both directions, with 403 PERMISSION_DENIED and the server's own text. The owner who holds manage_metadata is refused by the §5 operator gate, which names the isolated posture. That gate is the one that makes this row more than D1.
    • No refused attempt moved a switch: pass. Row ids, active and updated_at held, and _status held, after every attempt.
    • Pins: pass, 8/8 dogfood and 72/72 unit.

    Verdict: pass. Limits: the live boot ran isolated; the group arm rests on the unit pins. The Setup UI was not driven.

    Seen on the way. Neither item contradicts D2; both go to the seat.

    • Fixture. The showcase cannot boot walled as shipped, because no example app declares @objectstack/organizations. This run declared it worktree-locally. A repeatable walled showcase boot needs a carrier for that declaration.
    • Per-organization seed replay. Each organization/create logged 9 ERROR [SeedLoader] lines: the showcase's sys_business_unit seeds (bu_acme, bu_field_ops, bu_west_coast, bu_east_coast, bu_hq_finance) were refused as duplicates on the global id, then "Deferred reference UNRESOLVED after pass 2 — sys_business_unit.parent_business_unit_id …". It reproduced on both creations (North and South).

    The server is stopped, its database and data directory are deleted, and the worktree is removed.


    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  16. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    Contributor

    F1 · pass — packaged permission sets lock, disable and clone exactly as they did on the pre-epic baseline, and none of it touches sys_metadata_activation · 2026-10-04T01:58Z

    Executor for claim 5975069117, revision 2 5975289769 (seat domain:services#2). Method: docs/qa/platform-checklist/RUNNER.md through the checklist-test skill; checklist item access-security.packaged-permission-set-lifecycle (rev 1). HTTP is the oracle; the Setup UI is the customer-view check.

    Environment

    • main eea82af67779f504bd5d53fccda3151066cdeaf6, a clean detached worktree with a full pnpm build (72/72). Console built fresh by pnpm objectui:build at the pin .objectui-sha ab187972159583b595facdcae3c73b50f6f312e9; pnpm check:console-sha green, every build canary passed, no engine relaxation.
    • Comparison baseline: the pre-epic commit 107bb4ba4b96bb74913e19b46f7756dd30029d4d (428f9b24a7^, the parent of the epic's first code commit), built in its own worktree (71/71). It already carries the Studio save of a package-declared permission set forks it into a silent, undiscoverable overlay #11513 lock (e170b0ae53 is its ancestor). The same probe script ran on it, so the comparison is a live measurement, not a reading of Studio save of a package-declared permission set forks it into a silent, undiscoverable overlay #11513's pins.
    • Three runs of one HTTP probe: (a) fresh main boot, port 38829, file:/tmp/os-12438-f1f2/data.db; (b) the baseline boot, port 38817, file:/tmp/os-12438-f3/data.db; (c) main booted on that same baseline database after the upgrade (the F3 environment). Every boot was OS_PORT=… objectstack dev --ui --seed-admin -p … -d file:…, stock single posture.
    • Personas: admin admin@objectos.ai (proven with GET /api/v1/auth/get-session); in each run a fresh member made with POST /api/v1/auth/admin/create-user and granted the packaged set showcase_contributor through a sys_user_permission_set row. Its probe verb is one only that set grants: PATCH /api/v1/data/showcase_project/:id on runs (b) and (c), POST /api/v1/data/showcase_field_zoo on run (a), because the baseline's showcase does not grant showcase_field_zoo to contributors yet.

    What was done, and what was seen (the packaged base is showcase_contributor, managed_by package, package_id com.example.showcase)

    1. Lock. PATCH /api/v1/data/sys_permission_set/:id with an object_permissions change, and again with a description change → 403 NOT_OVERRIDABLE on all three runs, with a byte-identical message on baseline and main:

      [Security] Permission set 'showcase_contributor' is declared by package 'com.example.showcase' and is locked in this environment — editing it here would silently fork it from the package, and the fork would win over every future upgrade with no signal. Clone it instead (the "Clone" action on the permission set, or POST /api/v1/data/sys_permission_set with a new name) and edit the clone — the clone is your organization's own set, and upgrades keep flowing to 'showcase_contributor' untouched.

      The six definition facets and updated_at were unchanged afterwards on every run.

    2. Disable / enable (row state). PATCH … {"active": false} → 200 and the row reads active false; the member's probe verb → 403 PERMISSION_DENIED while inactive; {"active": true} → 200 and the verb works again (200/201). The member's assignment row is present at every step. Identical on all three runs.

    3. Clone. The clone_permission_set action's own payload (new label + new name, the six carried facets, active: true) → 201 on all three runs. The clone reads managed_by admin, package_id null, admin_scope null, active; all six facets equal the base (weak for system_permissions and tab_permissions, which are empty on this base); no lineage-shaped key. Its definition is freely editable (the PATCH the base refused → 200, persisted). The same clone name again → 409 on all three runs.

    4. Setup UI on main (admin, form sign-in, fresh browser context): the record header offers Activate / Deactivate / Clone; Deactivate shows "Deactivate this permission set? Existing assignments stay in place but stop granting access until re-activated." and sends PATCH {active:false} → 200; Activate → 200; the Clone dialog marks "New Display Name" and "New API Name" required, says "Delegated-admin scope is not copied…", shows the facets as "Carried over unchanged (read-only)", and its submit → POST 201, toast "Permission set cloned". An in-place Edit → Update sends PATCH → 403 with the sentence in step 1, and the form renders "You don't have permission to save this record." (banner and toast).

    5. Not the central switch table.

      • On the baseline, GET /api/v1/data/sys_metadata_activation → 404 "Object 'sys_metadata_activation' is not registered": the ledger did not exist yet.
      • On main (runs a and c): the ledger read 0 rows before and 0 rows after the whole lifecycle, no permission row, no row naming the base or a clone.
      • Positive control on run (a): switching the packaged flow showcase_urgent_task_alert off, the packaged action showcase_mark_done off, and the packaged set showcase_executive off, side by side, leaves exactly two rows, flow:showcase_urgent_task_alert and action:showcase_mark_done. The set's off-state lands only in its own sys_permission_set.active column. All three were restored, and the two rows read active true.
      • The toggle doors refuse it: POST /api/v1/automation/showcase_contributor/toggle → 404 "Flow 'showcase_contributor' not found"; POST /api/v1/actions/_activation/sys_permission_set/showcase_contributor → 404 "…has no declaration — there is nothing to switch off. The activation ledger addresses DECLARED actions (ADR-0126 §4)." The ledger was unchanged after both.
      • The listings never carry it: GET /api/v1/automation/_status lists 30 flows and no permission set (29 on the baseline, also none); Setup › Packaged Automation has two sections, "Packaged flows" and "Packaged actions", and names no permission set on run (a) or run (c), where the two tables hold 30 and 145 rows.
    6. Across the upgrade itself (F3 environment): the set the operator switched off on the baseline (showcase_auditor) still reads active false after main booted on that database, and the clone made on the baseline is still managed_by admin, still listed in Setup (Inactive tab) and still editable (PATCH description → 200). The name | managed_by | active listing of all 18 permission-set rows is identical before and after the upgrade.

    Differences found between baseline and main, and where they come from. Two refusal envelopes on edge paths differ, and both come from a later permission-set fix, 8f6d83147b (2026-09-20), which is not one of the epic's PRs:

    • creating a set under the packaged base's own name: baseline 409 "permission set 'showcase_contributor' already exists" → main 403 NOT_OVERRIDABLE "…Choose a different name for your set, or clone 'showcase_contributor' (the "Clone" action on the permission set) and edit the clone." The lock's refusal now answers first, and it names the clone path;
    • the duplicate-name 409 now carries the code UNIQUE_VIOLATION; on the baseline it had no code.

    Both are still refusals that write nothing. Lock, disable and clone keep the same semantics, and neither difference routes through the activation ledger.

    Against 「包内权限集的锁定、停用、克隆(#11513 已有功能)行为与升级前一致——权限集不走新的中央开关表,这是裁定过的设计。」

    • 锁定一致 (the lock is unchanged): pass. Same 403 NOT_OVERRIDABLE, same sentence, definition untouched, baseline vs main. The same-name-insert envelope above is the one refinement.
    • 停用一致 (disable is unchanged): pass. A row-state flip on the kind's own active column, enforced live, assignments kept, on both sides of the upgrade, and an operator's pre-upgrade choice survives it.
    • 克隆一致 (clone is unchanged): pass. 201, every facet carried, admin_scope not copied, org-owned, editable, no lineage, on both sides.
    • 不走中央开关表 (not the central switch table): pass. No sys_metadata_activation row from any permission-set operation, against a positive control proving the ledger does record flow and action switches; neither toggle door accepts the name; neither listing shows it.

    Verdict: pass.

    Seen on the way, for the seat. None of these changes the verdict.

    • The checklist item's c6 (the Setup form should show the server's refusal text) still fails as on 2026-09-29: the form substitutes "You don't have permission to save this record." That predates the epic. objectui at the baseline's pin 190fbd01d0 substitutes the same string for every permission error unconditionally (read from source; that console was not built). It is the The ownership-transfer refusal names the field it refused, but sets no userMessage, so every console substitutes an opaque 「no permission to save」 (from objectui#10108) #19397 class, closed not_planned.
    • Every boot of main on a database that holds a clone logs one WARN per clone: [permission_set_declaration_unowned] declared permission set "…" has no owning package — not materialized … the Setup admin surface reads sys_permission_set and cannot see this set. That is false for a clone: the row exists and Setup lists it. Details are in the F3 reply.

    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  17. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    Contributor

    F2 · pass — in Studio, a packaged action's definition is read-only and sends no write; forged writes are refused with the server's own codes; the packaged flow is still locked as B1 measured · 2026-10-04T01:59Z

    Executor for claim 5975069117, revision 2 5975289769 (seat domain:services#2). Method: docs/qa/platform-checklist/RUNNER.md through the checklist-test skill. The flow half re-confirms studio-authoring.packaged-automation-studio-lock (rev 1) briefly, as B1 already measured it in 5975204933. The action half has no checklist item of its own, so it was driven from the card row.

    Environment

    • main eea82af67779f504bd5d53fccda3151066cdeaf6, a clean detached worktree with a full pnpm build (72/72).
    • Console built fresh by pnpm objectui:build at the pin .objectui-sha ab187972159583b595facdcae3c73b50f6f312e9. The dist stamp equals the pin, pnpm check:console-sha is green, and the bundle, single-zod and spec-injection canaries passed. No engine relaxation was needed, as for B1 at this pin.
    • Boot: OS_PORT=38829 objectstack dev --ui --seed-admin -p 38829 -d file:/tmp/os-12438-f1f2/data.db, stock single posture. GET /api/v1/packages reports com.example.showcase writable: false.
    • Persona: admin admin@objectos.ai, form sign-in, each browser pass in its own fresh context (headless Chromium through Playwright). Every non-GET request under /api/ was recorded. The write count below excludes only sign-in, two read-only POST endpoints (/analytics/dataset/query, /security/explain) and the console's own sys_user_preference recents write.

    What was done, and what was seen

    The packaged action in Studio. The action is showcase_mark_done ("Mark Done", type script, object showcase_task, _packageId com.example.showcase, _provenance package).

    1. Path. Studio › ObjectStack Showcase › Data › Task › Advanced › Actions (/_console/studio/com.example.showcase/data?surface=object:showcase_task). This is the Studio surface where an object's actions are authored: they are edited on the object draft and saved with it. A screenshot confirmed it rendered before any DOM read. It lists "Actions (10)", and the header and the rail footer carry the package's "Read-only" badge.
    2. No authoring affordances. The list has no add or delete control. The only Save-type control, Publish, is disabled.
    3. The inspector is read-only. With "Mark Done" selected, every inspector input is disabled or readOnly. That covers Label, Name, Script body, the visible predicate, Success message and the "More fields" section. The first twelve comboboxes read (Object, Icon, Variant, Type, What it does, Script language, Execution, …) are all disabled. The same census on "Reassign…" (type flow) and "Recalculate Estimate" (type api) found the same: no editable control except the rail's "Search objects…" box.
    4. No write leaves the browser. I tried a forced fill of Label, typing into it, a forced click on Publish, and opening the Type and Object dropdowns with a keyboard pick. Then I waited 7 seconds. Label still read "Mark Done", and zero write requests were sent in two independent browser passes.
    5. The Setup metadata admin's page for the same action (/_console/apps/com.objectstack.setup/metadata/action/showcase_mark_done) agrees. It shows "This action is provided by an installed package, so it is read-only at runtime. To change it, edit it in its source package and republish — or create a new action from scratch.", has no editable input, has no save, publish or delete control, and sent zero writes.

    Forged writes, the server as the authority (admin; the body was the served definition with the label changed):
    6. PUT /api/v1/meta/action/showcase_mark_done and the same with ?mode=draft → 403 NOT_OVERRIDABLE:

    Metadata item 'action/showcase_mark_done' is provided by a code package, and its packaged base is locked against in-place edits. Switch it off (POST /api/v1/actions/_activation/:object/:action, body {enabled: false}, :object = global for an object-less action; operator-only where one install serves several organizations). See docs/adr/0126-packaged-metadata-customization-model.md.

    1. The same two with &package=com.example.showcase → 403 ITEM_LOCKED "Cannot overlay 'action' in package 'com.example.showcase': that package is read-only, and its packaged base is locked against in-place edits. Switch it off (…)".
    2. The object-draft door the Actions view saves through, PUT /api/v1/meta/object/showcase_task?mode=draft&package=com.example.showcase with the edited action in actions[] → 403 ITEM_LOCKED "Cannot overlay 'object' in package 'com.example.showcase': that package is read-only …".
    3. POST /api/v1/meta/action/showcase_mark_done/publish → 404 NO_DRAFT, because nothing was staged. DELETE /api/v1/meta/action/showcase_mark_done → 200 "No customization overlay found for action/showcase_mark_done — already at artifact default." That is the overlay-reset door answering a no-op, not a removal: the action is still served, byte-identical (step 10).
    4. GET /api/v1/meta/action/showcase_mark_done, GET /api/v1/meta/object/showcase_task and GET /api/v1/meta/flow/showcase_urgent_task_alert were byte-identical before and after all the probes, and GET /api/v1/meta/_drafts → {"drafts":[]}.

    The packaged flow, re-confirmed briefly (B1 measured it fully in 5975204933):
    11. Studio › Automations, ?surface=flow:showcase_urgent_task_alert, in a fresh context. The header switch is disabled, titled "Read-only package — switch to or create a writable package to edit.", and Publish is disabled. There is no "New" button, no "Add node" and no "Insert node". A forced click on the switch left it checked; a node click plus the Delete key and a 7-second wait sent zero writes.
    12. Forged: PUT /api/v1/meta/flow/showcase_urgent_task_alert?mode=draft&package=com.example.showcase → 403 ITEM_LOCKED; without package → 403 NOT_OVERRIDABLE ("…locked against in-place edits. Clone it under a new name to customize it (POST /api/v1/automation/:name/clone, body {name, label}), or switch it off …"). The definition was byte-identical afterwards (step 10).

    Against 「包内流程/动作的定义本体在 Studio 中仍不可原地编辑。」

    • 动作的定义本体不可原地编辑 (a packaged action's definition cannot be edited in place in Studio): pass. Read-only badge, every inspector control disabled or read-only, no add, delete or publish, and zero writes sent. Behind the courtesy layer, the server refuses every forged write door: /meta/action 403 NOT_OVERRIDABLE / ITEM_LOCKED, and /meta/object draft 403 ITEM_LOCKED. The definition is byte-identical afterwards.
    • 流程的定义本体不可原地编辑 (a packaged flow's definition cannot be edited in place in Studio): pass, consistent with B1. Switch disabled, no authoring affordance, zero writes, and the forged draft writes answer 403.

    Verdict: pass.

    Seen on the way, for the seat. Neither changes the verdict.


    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  18. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    Contributor

    F3 · pass — a pre-epic showcase database boots on main with no migration step; every packaged flow and action reads enabled; the baseline data, runs, grants and operator choices read back; a packaged flow still fires; 0 ERROR lines · 2026-10-04T02:00Z

    Executor for claim 5975069117, revision 2 5975289769 (seat domain:services#2). Method: docs/qa/platform-checklist/RUNNER.md through the checklist-test skill. No checklist item covers this row (the 2026-09-29 run recorded it blocked(fixture)), so it was driven from the card row. Nothing was simulated: no row was edited by hand, and the upgrade is a real boot of newer code on the older database file.

    Environment

    • Baseline: 107bb4ba4b96bb74913e19b46f7756dd30029d4d, which is exactly 428f9b24a7^, the parent of the epic's first code commit ("declare sys_metadata_activation…", feat(platform-objects): declare sys_metadata_activation, the ADR-0126 §4 activation ledger #12185). I used the commit itself rather than a release because it built in this container with the same toolchain as main (Node 22.22.0, pnpm 10.31.0, same packageManager pin); a full pnpm build passed, 71/71. The first attempt was OOM-killed in @objectstack/spec's DTS step while the console build ran beside it; that was memory contention, not toolchain drift, and the rebuild alone passed. It is pre-epic: no file under packages/ or examples/ mentions sys_metadata_activation at that commit, and Studio save of a package-declared permission set forks it into a silent, undiscoverable overlay #11513's lock (e170b0ae53) is an ancestor.
    • Target: main eea82af67779f504bd5d53fccda3151066cdeaf6, full build 72/72, console built fresh at the pin .objectui-sha ab187972159583b595facdcae3c73b50f6f312e9 (check:console-sha green).
    • One database file for both: file:/tmp/os-12438-f3/data.db, port 38817. Both boots used the same line, OS_PORT=38817 objectstack dev --ui --seed-admin -p 38817 -d file:/tmp/os-12438-f3/data.db, at the default log level (warn; neither OS_LOG_LEVEL nor LOG_LEVEL was set). --seed-admin only seeds an empty database, so on the second boot it did nothing.
    • Personas: admin admin@objectos.ai, seeded by the baseline boot; and a member created on the baseline with POST /api/v1/auth/admin/create-user.

    What was done, and what was seen

    On the baseline (pre-epic):

    1. Boot: 29 flows, 22 bound, 0 ERROR lines. GET /api/v1/data/sys_metadata_activation → 404 "Object 'sys_metadata_activation' is not registered". GET /api/v1/automation/_status → 29 flows, all enabled: true.
    2. Ordinary data: an account "QA F3 Baseline Account"; tasks "QA F3 baseline task A" (low) and "… B" (medium) on a seeded project. My project create was refused with the baseline's own validation, "Account is required", because my body omitted the account; I used a seeded project instead.
    3. A packaged-flow run: an urgent task fired showcase_urgent_task_alert, giving run run_81d1ca70…, completed. sys_automation_run then held 14 rows, including the seed's paused approval runs and connector runs.
    4. A permission-set assignment: a member granted the packaged showcase_contributor through a sys_user_permission_set row. Its contributor-only verb (PATCH /api/v1/data/showcase_project/:id) → 200.
    5. Operator choices: the packaged set showcase_auditor switched off ({active:false} → 200). F1's baseline probe also ran on this database. It added a second member and grant, and a clone qa_f1_baseline_clone, which was then edited and switched off.
    6. Stop: SIGTERM; the log ends "Server stopped", health went to 000, the port was freed, and the WAL was checkpointed into data.db. A read-only open of the file shows 89 tables and no sys_metadata_activation table.

    main on the same file, with no migration command:
    7. Boot succeeded: "Server is ready", health 200, console mounted. 0 ERROR lines in the boot log, and still 0 after all the requests below. The banner reads 30 flows, 20 bound, the same as a fresh main boot. No line asked for a migration before the server would serve. The lines that mention one:

    • four [schema-drift] lines report leftovers the newer schema no longer declares: the column sys_account.issuer and three generated indexes on sys_account, sys_verification and sys_job_queue. Each says "os migrate apply --allow-destructive" to drop it. That is an optional destructive cleanup, nothing depended on it, none of it concerns ADR-0126, and I did not run it;
    • one [Protocol] line: the stored baseline clone "carries a pre-protocol shape" (allowRestore), and it was converted on load by the ADR-0087 conversion permission-allow-restore-purge-removed, with no action.
    1. The ledger exists, empty: GET /api/v1/data/sys_metadata_activation → 200, 0 rows. The boot created the table itself.
    2. Every packaged flow and action reads enabled:
      • _status lists all 30 packaged flows with enabled: true.
      • Setup › Packaged Automation (admin, browser): "Packaged flows" 30 rows, 30 On; "Packaged actions" 145 rows, 145 On; all 175 switches checked, none off.
      • A packaged action still dispatches: POST /api/v1/actions/showcase_task/showcase_mark_done/:id on baseline task A → 200, and the task now reads done true, progress 100. There was no ACTION_DISABLED.
    3. The baseline data reads back:
      • the three tasks (titles and priorities intact) and the account;
      • the baseline run run_81d1ca70… is still listed, completed. All 14 baseline sys_automation_run rows are present (diff by id: 0 missing);
      • the member's assignment row still links the same user and set. The member signs in, and the contributor-only verb → 200;
      • showcase_auditor still reads active false, so the operator's choice survived. The name | managed_by | active listing of all 18 permission-set rows is identical before and after;
      • row counts: tasks 13 = 13, accounts 15 = 15, field-zoo 2 = 2. Projects went 5 → 6: the seed upserts projects keyed on name, and my probes had renamed the seeded "Data Platform" project, so the main boot's seed re-created one under that name. All five baseline project ids are still there.
    4. A packaged flow still runs: a new urgent task gave a new showcase_urgent_task_alert run, run_11e755f5…, completed.

    Against 「在一个升级前已有数据的环境升级到最新 main → 无需任何迁移动作,所有包内工件默认启用,已有功能无异常。」

    • 无需任何迁移动作 (no migration action needed): pass. No migration command was run, the boot served at once, it created the ledger table itself, and it converted the one legacy stored shape on load. The [schema-drift] lines offer an optional --allow-destructive cleanup of four pre-existing leftovers outside ADR-0126; I read them as advisory, not as a step the upgrade needs. The maintainer's spot-check can say otherwise.
    • 所有包内工件默认启用 (every packaged artifact enabled by default): pass. 30/30 packaged flows and 145/145 packaged actions read On with an empty ledger, and a packaged action dispatches. Packaged permission sets keep their own active column (card boundary 4): all read active except showcase_auditor, which the operator switched off before the upgrade, and that choice is kept.
    • 已有功能无异常 (existing functions work normally): pass. Records, runs, grants and operator choices read back; a packaged flow fires; a packaged action dispatches; 0 ERROR lines at the default level.

    Verdict: pass. Environment cleanup: both servers are stopped, /tmp/os-12438-f* is deleted, and the worktrees are removed.

    Seen on the way, for the seat (does not change the verdict; WARN, not ERROR):

    • Every main boot on a database holding an org-owned permission set logs, once per set: WARN [security] [permission_set_declaration_unowned] declared permission set "qa_f1_baseline_clone" has no owning package — not materialized. … the Setup admin surface reads sys_permission_set and cannot see this set ….
    • For a clone, that is false. The row exists (managed_by admin), Setup lists it in the Inactive tab, and it stays editable.
    • Reproduced twice: on this upgraded database (one clone, one line), and on a restart of a fresh main database that never saw the baseline (two clones, two lines; one of them was made with the Setup Clone dialog). So the cause is not the upgrade, and every environment that has ever used the Clone action gets these lines at every boot.
    • Mechanism, read and not pinned: bootstrapDeclaredPermissions (packages/plugins/plugin-security/src/bootstrap-declared-permissions.ts, at eea82af677) walks every permission item in the engine registry. The same boot registers the environment's own sets there ("[Registry] Registered permission: qa_f1_baseline_clone" appears in the same log). The loop takes ps._packageId ?? ps.packageId, and upsertPackagePermissionSet reports "unowned" before it reads the existing row, which would have classified the set as environment-authored.

    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  19. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    Contributor

    Acceptance complete: every row is ticked on evidence · closing completed · seat domain:services#2 (#21118) · session session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-04T02:03Z

    Every row of this card is now ticked. Each tick rests on a live-boot evidence reply, and none was simulated.

    Row Evidence
    A1–A2, C1–C2, D1, E2 and the rest earlier rounds (see the thread)
    A3, A5, B2, E1 first run under ruling B: 5968196694, 5968201206, 5968211463, 5968216948
    B1 HTTP half 5968206129; Studio half 5975204933, re-run on a console built at the pin ab18797215
    D2 5975356896: a live isolated boot, where only the platform admin flips the packaged flow and action switches
    F1 5975590364: packaged permission sets lock, disable and clone exactly as before, and the central switch table is never touched
    F2 5975593929: packaged action and flow definitions are read-only in Studio, and forged writes get 403
    F3 5975597571: a pre-epic database (428f9b24a7^) boots on main with no migration step. Every packaged artifact is on by default, and the data reads back

    F1–F3 were run on the maintainer's delegation, 「12438 你决定就好」 (decision 5975289769).

    Filed from these runs:

    None contradicts a row.

    Noted for the checklist sweep, not carded:

    The maintainer can reopen this card if a spot-check disagrees.


    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

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

Metadata

Metadata

Assignees

Labels

area:workflowApprovals and automation — the work that runs without a person driving itdomain:servicespriority:p2Medium: important, M3tracking

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions