Description
domain:devx#1 · os-warren · session_01JQwxbX9avDwM2goWd7zimE
Filing gate: ① a product defect, found beside the clauses (studio-authoring.api-written-keys-survive-studio-save), reach: the Studio autosave, measured twice.
Business reason: a dashboard draft can carry an audience gate (requiredPermissions) that its author does not hold, for example one written over the API or MCP by another author. When that author opens it in Studio, Studio shows "Draft · 1 unpublished" but edits the published body, and the next autosave replaces the draft without the gate and without If-Match. Publishing then serves the dashboard to everyone: the gate fails open, and every other draft-only change is lost too. This is the data loss the item's negative[0] forbids. It is not an authorization hole: the author is a metadata writer on their own package.
Reader who acts: the triage seat, then a domain:ui implementer in ui/packages/app-shell/src/views/metadata-admin/ and studio-design/. QA suggestion only: area:studio, p2.
Duplicate search: readDraftForSave → 6 hits; #26837 (closed, a permission-set save dropping other doors' values) is the nearest and a different path; requiredPermissions draft 403 Studio dashboard → 34 hits, none about this.
Batch-of: release-18.3.0-w4-c3b1 — 16 cards from QA run #29002 (subject 7fa9873f; the implementer re-checks the landing site on current main).
Reproduction
- A writable package PKG with a published dashboard D (one metric widget, no requiredPermissions).
- As admin:
PUT /api/v1/meta/dashboard/D?mode=draft&package=PKGwith the published body plus"requiredPermissions":["showcase.export_data"](a capability the admin holds through no set) → 200 draft. GET /api/v1/meta/dashboard/D?state=draft&package=PKG→ 403 PERMISSION_DENIED "You do not have permission to open the 'D' dashboard." (by design,packages/rest/src/meta-item-read-gate.ts).- Form sign-in →
/_console/studio/PKG/interfaces→ D in the Dashboards rail. The header shows "Draft · 1 unpublished"; the canvas shows the published body (network: the draft read 403, then/layers200). - Edit the Label and wait 2 s.
Expected: Studio refuses to edit a draft it cannot read, or keeps the draft's keys; at least it never overwrites it unpinned. Actual: PUT …/dashboard/D?mode=draft&package=PKG&surface=studio → 200 with no requiredPermissions and no If-Match; the draft read afterwards has no gate. Reproduced 2 of 2 on fresh loads.
Mechanism (suspected)
readDraftForSave in ui/packages/app-shell/src/views/metadata-admin/DraftConflictDialog.tsx catches any draft-read failure (403 included) and answers "no pending draft", so StudioDesignSurface.tsx (leaf load, about L5186–5216) seeds the leaf from the layered baseline and the save goes out with no pin.
QA-source: #29002 · studio-authoring.api-written-keys-survive-studio-save · observed beside the clauses
Activity
objectstack-fleet commentedon Nov 2, 2026
domain:devx#1 · os-warren · session_01JQwxbWBtYWqaV8NhvUrShB
Ruling: release blocker for 18.3.0 · maintainer · 2026-11-02T07:11Z
Warren ruled this card blocks the 18.3.0 release. The question put to him in the project thread "Plan the release checklist run" read, verbatim: 「两个回归 #28982、#28986,及 #29015 是否挡版。我的看法是 #29015 值得挡(权限门会失效),另外两个是文案和提示上的问题,可以不挡。」 His answer there (2026-11-02T07:06Z): 「待定全部同意」. So #29015 blocks; #28982 and #28986 do not.
target:v18added by this act on the maintainer's ruling (the release board's producer is the director seat; this stroke carries his word, so it is not a seat's own grading).requiredPermissions) that fails open after a Studio autosave, plus draft data loss.domain:*,area:*, priority) — this card carries no labels yet, and the ruling does not grade it. The fix lands inui/packages/app-shell/src/views/metadata-admin/per the body.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: studio-authoring.api-written-keys-survive-studio-save
Triage:
domain:ui· p1 ·pm:queue· 2026-11-02T07:24Z⛔ Not a claim.
target:v18. A card targeted at the release is release-first, so p1, not p2 (「发版本优先的都p1」).Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu
Claim: PM loop round 19 · 2026-11-02T07:30Z
Session:
session_01QsR2jT5xpWAfUCpLoqcxaT(card thread, os-dev); dispatched by seat sessionsession_01QsR2jUJ19MstFvFhjgiYTuAccount:
zhuangjianguoBranch:
claude/project-thread-auz07c(platform-assigned to the card thread; it can push only this branch)Worktree:
objectstack-issue-29015Domain:
domain:uiSeat:
domain:ui#1File surface:
ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx(the draft read path),ui/packages/app-shell/src/views/metadata-admin/DraftConflictDialog.tsx, their tests, and a root.changeset;packages/rest/src/meta-item-read-gate.tsread only (stop on breach; explain in the report)Container & model: M,
mode:thread,model: default tier (dispatch-gates --tier: no path-derived mandate),effort: highClause-②: no
Metadata-protocol: none
Responsibility: Studio reads a refused (403) dashboard draft as "no draft" and autosaves over it
Thread-read: 6571523054
Premise: holds (at
origin/main5d3667e2: the card measures a 403 draft read on requiredPermissions treated as no draft; the dev re-measures before code)Serial constraints cleared: PRs #29081 and #29073 (queued) edit
StudioDesignSurface.tsxin other hunks; #28984 waits for #28986.objectstack-fleet commentedon Nov 2, 2026
domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu
Claim amended · 2026-11-02T07:34Z · after the dev's measurement at
origin/main5d3667e2. This adds to the file surface and adds to the round 19 claim on this card. Branch, session and size are unchanged.readDraftForSave(DraftConflictDialog.tsx) catches everygetDraftfailure and answers{answer: null, pin: null}; the client throws on 403 and resolvesnullonly on 404. So the Interfaces leaf load seeds from/layersand the next save goes unpinned over the refused draft. The server's 403 is by design (audienceRefusalinmeta-item-read-gate.ts), so the fix is client-side andpackages/reststays untouched.readDraftForSavekeeps its shape and gains an optionalrefusedmember for a 403 (existing callers and pins unchanged). The Interfaces leaf load stops onrefused: no buffer, no canvas, no inspector, so the autosave has nothing to send. The strip says the draft is unpublished and this session may not read it, so Studio did not open it, and that someone holding its required permissions can edit or publish it; the server's sentence goes in Details.ui/packages/i18n/src/namespaces/studio/(en.ts,zh.ts).Record<string, string>and shipped under the separate./namespaces/studioexport, not the locale packs whose types list literal keys (the Studio: an All metadata area in the builder — directory, lists, generic item page, history and diagnostics, per package and across packages; pillar types open their pillar #28561 ruling). Metadata-protocol stays none.readDraftForSavecallers (ResourceEditPage,ObjectListViewInspector,StudioBundleItemEditor,PermissionMatrixEditor,ObjectHooksPanel). The dev reports them; the seat files them.objectstack-fleet commentedon Nov 2, 2026
domain:ui#1 · zhuangjianguo · session_01QsR2jT5xpWAfUCpLoqcxaT
os-dev-report
{ "issue": 29015, "status": "done", "branch": "claude/project-thread-auz07c", "pr": "https://github.com/objectstack-ai/objectstack/pull/29132", "session": "session_01QsR2jT5xpWAfUCpLoqcxaT", "premise_still_valid": true, "summary": "Measured at origin/main 5d3667e2: readDraftForSave caught every getDraft failure (the client throws on 403 and resolves null only on 404) and answered no draft, so the Interfaces leaf load seeded from /layers and the next autosave went unpinned over the gated draft. readDraftForSave now hands a 403 back as an optional `refused` member (isDraftReadRefused tells it apart by status; every other outcome and pin is unchanged), and the leaf load installs no buffer on `refused`: no canvas, no inspector, nothing for useDraftAutoSave to send. The strip names the cause (engine.studio.refusal.draftUnreadable, en + zh studio tables) with the server sentence behind Details. packages/rest untouched: the server 403 is by design.", "tests": "head 16e3c584. New StudioDesignSurface.draftUnreadable-29015.test.tsx + DraftConflictDialog.test.tsx: 2 files, 27 passed. Ablation via scripts/ablation-replace.mjs (anchor `if ('refused' in draftRead) {` -> `if (false && 'refused' in draftRead) {`, blob 8fb9468a -> a2d564b7 and restored to HEAD blob, git diff HEAD empty): refused-leaf case red (Unable to find the draftUnreadable text), control green. Nearby app-shell + studio i18n readers (64 files naming readDraftForSave / DraftConflictDialog / InterfacesPillar): 737 passed. packages/i18n/: 95 files, 1443 passed, 13 skipped. ui scripts/: 169 files passed, 2 skipped, 4900 tests passed. turbo type-check app-shell + i18n: 66/66 successful. All under os-verify-lock, VERDICT command-exit 0 each.", "gates": "dispatch-gates --commands derived 34 families; 33 in one loop + `pnpm --dir ui exec vitest run scripts/`, every one exit 0; --ran: 34 derived, 34 run, 0 NOT-MEASURED. PR-body gates on the exact body: check-changeset-no-major, check-adr-0087-registration, check-closing-keyword-parity, check-partof-closing-keyword all exit 0. check-sdui-manifest exit 0. check:new-line-citations 0 new. CI on #29132: in_progress at report time (20 check-runs listed, none failed); convergence is the seat's.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "4 — issue_read x2 (card + comments), pull_request_read get_check_runs x1, plus ToolSearch schema load; no MCP write", "api_writes": "2 — git push of claude/project-thread-auz07c; relay pr_create (fleet-write run 41410521894, objectstack-ai/objectstack#29132, body read back identical); this report comment is the third", "open_questions": [], "out_of_scope_findings": [ "class: a · reach: the same 403-as-no-draft reading on the Interfaces APP nav load in StudioDesignSurface.tsx (an app draft carrying requiredPermissions is refused by the app arm, readDraftForSave answers null, the nav buffer seeds from the published layer and the nav save goes unpinned) — by code reading, not run · family with #29015 · dedupe words: readDraftForSave app nav requiredPermissions 403", "class: a · reach: the other readDraftForSave callers read a 403 the same way: ResourceEditPage, ObjectListViewInspector (list views carry the same audience gate), StudioBundleItemEditor, PermissionMatrixEditor, ObjectHooksPanel; each can now ask `refused` — by code reading, not run · one family card · dedupe words: readDraftForSave refused 403 draft overwrite" ] }objectstack-fleet commentedon Nov 2, 2026
domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu
Landed · 2026-11-02T09:52Z · PR #29132 merged through the merge queue as
3d2e8fdbat 2026-11-02T09:52Z (GitHub's clock). ACCEPT 6572493118.3d2e8fdb~1)origin/main)draftUnreadableinui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsxStudioDesignSurface.draftUnreadable-29015.test.tsxexistsFixes #29015closed this card.pm:dispatchedcomes off in the same stroke as this note. #28984 and #29148 waited on this PR, and both can now be ordered onorigin/main. #29113 shares the sameStudioDesignSurface.tsxand is unblocked too.