Skip to content

Studio Access: creating a permission set whose derived id equals an already-published set puts a blank draft over the live set (publishing it would wipe its grants), and Studio offers no way to discard it #26836

Closed

Description

@objectstack-fleet

Filing gate: ① product defect, reproduced, landing site named.
Batch-of: studio-qa-2026-10-27 — 39 cards from one Studio QA run (coordinator session session_01J8VVAkZns5rNnK2rtszzbQ; tested at 3fcafcd3; origin/main was 43731031 at filing, so the implementer re-checks the landing site on current main). The run record is the qa-run issue filed with this batch.

Symptom

The create dialog's first-write guard only checks for an existing DRAFT row, so a name that derives to a published id silently starts a blank replacement of a live set.

Reproduction

  1. Publish project_editor_qa.
  2. Access › "+" › Name "Project Editor (QA)" → Save as draft → toast "created"; GET /api/v1/meta/permission/project_editor_qa?state=draft → {objects:{},fields:{}}.
  3. There is no discard control in Studio; publishing would replace the live set with an empty one. Coordinator API-confirmed.

Expected

Creation refuses an id that already exists in any state (offering "open existing"), and Studio can discard a draft.

Suspected landing site (the implementer verifies)

AccessPillar doCreate (ifNoneMatch: "*" guards only the draft row).

Filing facts

Activity

  1. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — priority:p1 · domain:ui · area:access · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-27T18:56Z

    ⛔ Not a claim, ⛔ not a dispatch.

    • p1: creating a permission set whose derived id equals a published set starts a blank draft over the live set, and publishing it would wipe its grants. The first-write guard checks the published row as well as the draft, and refuses with a plain message naming the existing set. If the metadata door also lets that write through, the door's half is a domain:server child. Re-check the landing site on current main.

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01QsR2jTfvjNTaR29ehLk26T
    Account: zhuangjianguo
    Branch: claude/project-thread-m5noqq (platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)
    Worktree: objectstack-issue-26836
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (AccessPillar only), studio namespace en.ts/zh.ts, tests beside, one root changeset; ⛔ packages/spec/**, server packages (a door half is reported as a domain:server child), any new key (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate; reason: the maintainer's standing rule 「只认领不需要 fable 的卡」, 2026-10-27T10:39Z), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: Studio's Access create path (ui app-shell, this lane) writes a blank draft over a published permission set, because its first-write guard covers the draft row only · the platform path that may cover it: the metadata door's If-None-Match: *, which the thread measures (a server child if it should refuse a published row) · reached by every "+ New" permission set whose name derives to a live set's id
    Thread-read: 6450910097
    Premise: holds (AccessPillar.doCreate saves with mode: 'draft', ifNoneMatch: '*' and no published-row check, StudioDesignSurface.tsx ~8544-8558, on origin/main c4a4aefc at 2026-10-27T20:19Z)
    Serial constraints cleared: open PR #26839 (#26705) touches AccessPillar's held-edit hook (~8473), a different region; PR #26824 touches the slot host only (scan of the 80 most recently updated open PRs 2026-10-27T20:20Z)

    Dispatched at 2026-10-27T20:22Z to the card thread on that branch; the order lives in the thread's opening message.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 26836,
      "status": "done",
      "branch": "claude/project-thread-m5noqq",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/26916",
      "session": "session_01QsR2jRu5CpdGJFtUFzPvUy",
      "premise_still_valid": true,
      "summary": "AccessPillar.doCreate now reads the published permission sets and the draft headers across every package (client.list('permission') + client.listDrafts({ type: 'permission' }), the Interfaces create's unscoped pair) before writing; a taken identifier is refused in the dialog with a sentence naming the set, plus an 'Open LABEL' path when the set is on this package's rail. A fresh identifier saves as before (mode draft, packageId, ifNoneMatch '*', surface studio). An open set with a pending draft gets 'Discard draft' in its header: confirm, then client.reset('permission', NAME, { state: 'draft', packageId }) (the metadata editor's discard door, existing API); a published set re-reads its live body (matrix publishNonce + local nonce), a never-published set leaves the rail. Six new studio-namespace rows (en + zh). Door measured: no server child (see open_questions).",
      "tests": "head 4dd29301. New StudioDesignSurface.accessCreateTaken-26836.test.tsx on the real AccessPillar + PermissionMatrixEditPage: Test Files 1 passed, Tests 9 passed (9), under os-verify-lock. Ablation via scripts/ablation-replace.mjs (anchor 'if (publishedRow || draftRow) {' -> 'if (false && (publishedRow || draftRow)) {', x1 -> x0): Tests 3 failed | 6 passed (the three refusal cases); restored, blob == HEAD b2086038, git diff HEAD empty; tests import the pillar from source, no build in between. app-shell + i18n type-check: VERDICT command-exit 0. Narrowed suites (66 app-shell files naming AccessPillar / studio namespace / engine.studio.access + all of packages/i18n/): Test Files 155 passed (155), Tests 2159 passed | 13 skipped. check:i18n-designer-parity exit 0; check:new-line-citations 0 new.",
      "gates": "dispatch-gates --commands derived 34 at 4dd29301; all 34 exit 0 (PR-body gates against the final body via GITHUB_EVENT_PATH, re-run after the Verification section: closing-keyword-parity 0, adr-0087-registration 0, changeset-no-major 0, partof-closing-keyword 0). vitest run scripts/: 178 passed | 2 skipped files, 5065 passed tests. check:type-check-debt: 0 raw tsc errors. dispatch-gates --ran: 34 derived, 34 run, 0 NOT-MEASURED. CI check-runs on #26916: in_progress at report time.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "4 — GitHub MCP issue_read x3 (get, get_comments x2) on #26836, all reads; claude-code-remote get_session x1. No MCP write.",
      "api_writes": "2 — relay repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches x2 (pr_create #26916 with assignees [zhuangjianguo], run 40583946894, read-back identical 7927 bytes; this os-dev-report comment). git push is not a REST write.",
      "line_budget": "5 files, +491 / -6 (dispatch-gates count at 4dd29301), under the 3000 human-merge threshold",
      "files_changed": [
        ".changeset/26836-access-create-taken-id.md",
        "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx",
        "ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.accessCreateTaken-26836.test.tsx",
        "ui/packages/i18n/src/namespaces/studio/en.ts",
        "ui/packages/i18n/src/namespaces/studio/zh.ts"
      ],
      "deviations": [
        "changeset carries '@object-ui/i18n': patch beside '@object-ui/app-shell': patch (the order named app-shell only); the new studio-namespace rows ship from @object-ui/i18n",
        "Discard reuses three existing rows (engine.edit.discardDraft, engine.edit.draftDiscarded) and adds two confirm sentences of its own: the shared engine.edit.discardDraftConfirmBody says the published version stays, which is false for a never-published set",
        "narrowed app-shell run (66 files), not the full package; CI Test UI runs the rest"
      ],
      "open_questions": [
        {
          "question": "Should the metadata door's draft-mode first-write pin (If-None-Match: * with ?mode=draft) also refuse when a PUBLISHED row exists? Measured: no defect today. SaveMetaItemRequestSchema.parentVersion declares a present null asserts 'no current row of this lifecycle', and the metadata-protocol changelog says the pin 'with ?mode=draft for a draft ... saves only where no row of that lifecycle exists'. The door does what the spec declares.",
          "options": [
            "A — keep the door as declared; each create surface checks published rows client-side (this PR for Access; Interfaces already does). Race window: a set published between the read and the save is not refused.",
            "B — a spec + server change making a draft-mode create pin judge every lifecycle (Clause-② semantic, domain:server + spec contract review), closing the race for every client."
          ],
          "recommendation": "A, because the lifecycle-scoped pin is a declared, legitimate meaning ('start a draft only if nobody has one') and the race needs a publish inside a sub-second window; B changes a published contract for every caller with no measured producer hitting the race."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed — Data and Automations creates (doCreateObject / doCreateFlow) carry the same draft-only first-write pin; doCreateObject checks only its own rail, doCreateFlow nothing. Not reproduced here (no measured overwrite of a published object or flow), so recorded only. Dedupe words: studio create published id blank draft, first-write pin lifecycle, doCreateFlow published flow overwrite"
      ]
    }

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:uipm:dispatchedpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions