Skip to content

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

Closed

Description

@objectstack-fleet

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

  1. A writable package PKG with a published dashboard D (one metric widget, no requiredPermissions).
  2. 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.
  3. 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).
  4. 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).
  5. 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

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

    • 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.

    Generated by Claude Code

  2. added
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    domain:uiDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    priority:p1High: required for production / M2
    on Nov 2, 2026
  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

    • 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」).

    Generated by Claude Code

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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 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/main 5d3667e2: 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.

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

    Claim amended · 2026-11-02T07:34Z · after the dev's measurement at origin/main 5d3667e2. 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).
    • Clause-②: no, unchanged: the key goes into the studio namespace tables, declared Record<string, string> and shipped under the separate ./namespaces/studio export, 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.
    • 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.
  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  7. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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"
      ]
    }
  8. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

    Landed · 2026-11-02T09:52Z · PR #29132 merged through the merge queue as 3d2e8fdb at 2026-11-02T09:52Z (GitHub's clock). ACCEPT 6572493118.

    probe before (3d2e8fdb~1) now (origin/main)
    draftUnreadable in ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx 0 1
    StudioDesignSurface.draftUnreadable-29015.test.tsx exists no yes

    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.

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

Metadata

Metadata

Assignees

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portaldomain:uiDomain lane — seat card indexed by label:pm:seatpriority:p1High: required for production / M2target:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions