Skip to content

A read-only external object offers New and Import, then refuses with a permission message that blames the wrong thing #30334

Closed

Description

@objectstack-fleet

epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff

Filing gate: ③ the maintainer's direct order, verbatim: 「你发现的问题都立卡,安全问题总专用的安全密卡通道,需要进一步分析的进一步测试,还是没有把握的用决策卡。」 (2026-11-05). Found in a real-user browser walkthrough of Setup on main 04f36b63f8, with a test app and a local test deployment.

Business reason. An administrator who is told 您没有权限 goes looking through permission sets, and no permission set can fix a table that is read-only by design. The buttons should not be offered in the first place.

Reader. The UI lane dev for the object list and form, reading what the server already knows about the object.

Duplicates. Searched external object read-only create button (46 hits) and external read only import button (45 hits). They are QA run anchors and epic #27314; none has this subject.

Measured

On object spare_parts, synced from a datasource in external mode:

  • The list shows 新建 and 导入, and the form opens normally.
  • Submitting shows 「您没有权限保存这条记录。」
  • The server's own answer is exact: Write 'insert' blocked on object 'spare_parts': datasource 'parts_db' is external (schemaMode=external). Requires datasource.external.allowWrites=true (got false) AND object.external.writable=true (got false). The form drops that reason.
  • The source table was not changed, so the protection itself is correct.
  • The list also shows a 在队列中 filter, though this object has no queue.

I did not click 导入, and did not check whether a record's edit and delete are offered.

Expected

  • A read-only external object does not offer create, import, edit or delete, or shows them disabled with the real reason.
  • If a write still reaches the server, the message says the table is read-only, not that the user lacks permission.
  • The queue filter appears only on objects that can be in a queue.

Activity

  1. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p2 · domain:ui · pm:queue · 2026-11-05T07:11Z

    ⛔ Not a claim.

    • Read fresh. A read-only external object offers New and Import, then refuses with a permission message that blames the wrong thing. Offered doors that cannot work, so p2.

    Generated by Claude Code

  2. added
    domain:uiDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 5, 2026
  3. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#3 · os-bill · session_01Utp6CbPN5UYMnBaNJPJWfG

    Claim: PM loop round 2 · 2026-11-05T08:15Z
    Session: session_01Utp6CajiubyEqtA9TdrtCf (card thread, os-dev); dispatched by seat session session_01Utp6CbPN5UYMnBaNJPJWfG
    Account: os-bill
    Branch: claude/project-thread-9t8krp (platform-assigned to the card thread; it can push only this branch)
    Worktree: objectstack-issue-30334
    Domain: domain:ui
    Seat: domain:ui#3
    File surface: the object list toolbar (New / Import) and record form / detail action gating as measured in ui/packages/plugin-list/**, plugin-grid/**, plugin-form/**, plugin-detail/** or ui/packages/app-shell/src/views/**, the form's save-error mapping, ui/packages/i18n/src/** strings, their tests, one root .changeset/ file; ⛔ packages/** (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default tier (opus) — dispatch-gates --tier at fdad1786: no path-derived mandate, effort: high
    Clause-②: yes (widening) — @object-ui/i18n keys expected; the dev measures and the seat corrects to no if none
    Metadata-protocol: none
    Responsibility: the console's list and form offer create/import/edit on an object the server refuses by ADR-0015 (external, not writable) | the server guard already refuses the write and names both flags; the client drops the reason | every user of a read-only external object
    Thread-read: 6652070429
    Premise: holds
    Serial constraints cleared: in flight domain:ui #30220 (plugin-form/src/WizardForm.tsx, plugin-kanban) and #30122 (plugin-detail/src/recordTimelineStream.ts, timeline renderers) — the dev keeps off those regions and reports any overlap. main red at claim (#30367), not this card's.

  4. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#3 · os-bill · session_01Utp6CbPN5UYMnBaNJPJWfG
    os-dev-report

    Card thread session session_01Utp6CajiubyEqtA9TdrtCf, PR #30403, head ae3c67c, reported 2026-11-05T09:43Z.

    {
      "issue": 30334,
      "status": "done",
      "branch": "claude/project-thread-9t8krp",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30403",
      "session": "session_01Utp6CajiubyEqtA9TdrtCf",
      "premise_still_valid": true,
      "summary": "Item 2 only, per the seat's claim ruling (PR says Refs #30334 (item 2)). The ADR-0015 write gate answers 403 EXTERNAL_WRITE_FORBIDDEN (measured live on app-showcase showcase_ext_customer for POST, PATCH and DELETE). New @object-ui/core isExternalWriteRefusal (re-exported from @object-ui/react) reads only that code, asked before isPermissionError; new key form.readOnlyExternalSource in ten packs. Seven surfaces now say the table is read-only: console FormPage, the record dialog (components form), record header delete, list row delete (core recordDelete), record inline edit (InlineEditSaveBar), calendar reschedule, path stage change. Before: the permission line on the two forms, the gate's raw English diagnostic on the three delete/inline surfaces.",
      "tests": "76473999 (later commits touch no ui file): targeted vitest under the lock, 199 files (4 new test files + every test importing a changed module + i18n package tests): Test Files 199 passed (199), Tests 2191 passed | 90 skipped, VERDICT command-exit 0. turbo type-check over core, react, i18n, components, plugin-calendar, plugin-detail, app-shell, console: Tasks 81 successful, 81 total, VERDICT command-exit 0. Live walk (app-showcase objectstack dev --fresh + console Vite, zh): create, edit, header delete, row delete, inline edit, before (sources at fdad1786) and after; shots in project files ui-seat3/30334/. No ablation: the new tests carry their own controls and the before leg is the live walk.",
      "gates": "ae3c67c2: dispatch-gates --commands derived 35 families, all 35 run with exit 0 recorded before any pipe (ui-seat3/30334/gates-ran.txt in project files), --ran reconciliation: 35 derived, 35 run, 0 NOT-MEASURED, exit 0; check-sdui-manifest among them; ui check:new-line-citations 0 new. CI check-runs: in_progress at report time; a Lint UI red on check:liveness (query/searchFields) is main's #30367.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "4 — mcp__github__issue_read x4 (get x1, get_comments x3), all reads, before the claim and at claim check; no MCP write",
      "api_writes": "2 — relay pr_create (POST /repos/objectstack-ai/objectstack/pulls, draft), post-stamped POST /repos/objectstack-ai/objectstack/issues/30334/comments (this report)",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: #30370 · hiding New/Import/Edit/Delete: /auth/me/permissions answers allowCreate/allowEdit/allowDelete true and apiOperations [get,list,create,update,delete,upsert,bulk,aggregate,search,import] for showcase_ext_customer (measured live); buildEffectiveObjectPermissions never reads the ADR-0015 gate",
        "carrier: #30372 · the 在队列中 filter: ListView offers it on hasAnyQueue AND the served system queue column; spec resolveInjectedSystemColumns plans queue on external objects",
        "carrier: #30220 · plugin-kanban card move reads the same refusal as the permission line (isPermissionError before extractWriteErrorMessage); left untouched, the package is held by that claim"
      ],
      "line_budget": "27 files, +458 / -15 vs merge base (of which 4 new test files and the changeset)",
      "deviations": [
        "File surface: the claim names plugin-list/grid/form/detail and app-shell views; the measured refusal mapping also lives in ui/packages/react/src/utils/error-message.ts and index.ts (re-export), ui/packages/core/src/utils/external-write-refusal.ts, core/src/index.ts and core/src/actions/recordDelete.ts (the list row delete door), ui/packages/components/src/renderers/form/form.tsx (the record dialog), ui/apps/console/src/components/FormPage.tsx and ui/packages/plugin-calendar/src/ObjectCalendar.tsx. None under root packages/**.",
        "The harness's after-PR assignee and review-request step was skipped, per the dispatch order (the seat hands off).",
        "The harness's model-named co-author trailer was refused by the pre-push check:commit-card-trailers; the unpublished commit was amended to the model-free pair before the first push."
      ],
      "files_changed": [".changeset/30334-external-write-read-only-line.md", "ui/apps/console/src/components/FormPage.externalWriteRefusal-30334.test.tsx", "ui/apps/console/src/components/FormPage.tsx", "ui/packages/app-shell/src/views/RecordDetailView.tsx", "ui/packages/components/src/__tests__/form-write-error-external-30334.test.tsx", "ui/packages/components/src/renderers/form/form.tsx", "ui/packages/core/src/actions/recordDelete.ts", "ui/packages/core/src/index.ts", "ui/packages/core/src/utils/external-write-refusal.ts", "ui/packages/i18n/src/locales/ar.ts", "ui/packages/i18n/src/locales/de.ts", "ui/packages/i18n/src/locales/en.ts", "ui/packages/i18n/src/locales/es.ts", "ui/packages/i18n/src/locales/fr.ts", "ui/packages/i18n/src/locales/ja.ts", "ui/packages/i18n/src/locales/ko.ts", "ui/packages/i18n/src/locales/pt.ts", "ui/packages/i18n/src/locales/ru.ts", "ui/packages/i18n/src/locales/zh.ts", "ui/packages/plugin-calendar/src/ObjectCalendar.tsx", "ui/packages/plugin-detail/src/InlineEditSaveBar.tsx", "ui/packages/plugin-detail/src/renderers/__tests__/stageRefusalLines.externalWrite-30334.test.ts", "ui/packages/plugin-detail/src/renderers/recordPathStageChange.tsx", "ui/packages/plugin-detail/src/useDetailTranslation.ts", "ui/packages/react/src/index.ts", "ui/packages/react/src/utils/error-message.externalWriteRefusal-30334.test.ts", "ui/packages/react/src/utils/error-message.ts"]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#3 · os-bill · session_01Utp6CbPN5UYMnBaNJPJWfG

    Release: 2026-11-05T10:34Z — PR #30403 landed (merge commit a91409c42, merged 2026-11-05T10:31Z; seat ACCEPT 6655338629 on head b43a9857), Refs #30334 (item 2), so this card stays open.

    Resume-from: none

    Back to pm:queue; triage may close this card in favour of #30370 / #30372.

  6. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Nov 5, 2026
  7. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Closed as completed · 2026-11-05T16:43Z

    This answers domain:ui#1's ask (6663640225), under the maintainer's 2026-11-05 ruling that triage closes a tracking parent once every carried row is closed. Read fresh before this stroke.


    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

No one assigned

    Labels

    bugSomething isn't workingdomain:uiDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions