You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Studio: a dashboard draft whose requiredPermissions the author does not hold reads 403, Studio treats that as "no draft", and the next autosave overwrites the draft without the key #29015
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=PKG with 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 /layers 200).
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
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.
Label: target:v18 added 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).
Blocker class: ② a declared audience gate (requiredPermissions) that fails open after a Studio autosave, plus draft data loss.
Still owed: triage first-touch (domain:*, area:*, priority) — this card carries no labels yet, and the ruling does not grade it. The fix lands in ui/packages/app-shell/src/views/metadata-admin/ per the body.
Read fresh. readDraftForSave in ui/packages/app-shell/src/views/metadata-admin/DraftConflictDialog.tsx treats a 403 draft read as no draft, so StudioDesignSurface.tsx seeds from the published layer and overwrites the draft unpinned; domain:ui. Silent loss of draft keys is a user-visible defect, p2, and the filer states the author is an authorized writer, so no SECURITY flag. It was seen beside the clauses and the item did not FAIL in the run, so the release rule does not apply.
Re-read just before writing: the card now carries target:v18. A card targeted at the release is release-first, so p1, not p2 (「发版本优先的都p1」).
Claim: PM loop round 19 · 2026-11-02T07:30Z
Session: session_01QsR2jT5xpWAfUCpLoqcxaT (card thread, os-dev); dispatched by seat session session_01QsR2jUJ19MstFvFhjgiYTu
Account: zhuangjianguo
Branch: claude/project-thread-auz07c (platform-assigned to the card thread; it can push only this branch)
Worktree: objectstack-issue-29015
Domain: domain:ui
Seat: domain:ui#1
File 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.ts read only (stop on breach; explain in the report)
Container & model: M, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: high
Clause-②: 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.tsx in other hunks; #28984 waits for #28986.
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.
Measured:readDraftForSave (DraftConflictDialog.tsx) catches every getDraft failure and answers {answer: null, pin: null}; the client throws on 403 and resolves null only on 404. So the Interfaces leaf load seeds from /layers and the next save goes unpinned over the refused draft. The server's 403 is by design (audienceRefusal in meta-item-read-gate.ts), so the fix is client-side and packages/rest stays untouched.
Shape:readDraftForSave keeps its shape and gains an optional refused member for a 403 (existing callers and pins unchanged). The Interfaces leaf load stops on refused: 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.
File surface (added): one new key in ui/packages/i18n/src/namespaces/studio/ (en.ts, zh.ts).
Out of scope: the same defect on the app nav load and the other readDraftForSave callers (ResourceEditPage, ObjectListViewInspector, StudioBundleItemEditor, PermissionMatrixEditor, ObjectHooksPanel). The dev reports them; the seat files them.
{
"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"
]
}
Fixes #29015 closed this card. pm:dispatched comes off in the same stroke as this note. #28984 and #29148 waited on this PR, and both can now be ordered on origin/main. #29113 shares the same StudioDesignSurface.tsx and is unblocked too.
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.