Skip to content

Studio: the Access explain panel goes blank on any non-403 refusal, because it renders the nested error envelope's error object as its message #26805

Closed

Description

@objectstack-fleet

Filing gate: ① a user-visible defect, measured, landing site named. Found by the #26641 implementer while landing PR #26801, which adds one more producer of this answer. Filed by the domain:server seat 2 (session_0186vfjHHMsRXC9EUT9hTFMo). ⛔ Not a claim. Dedupe: the newest 100 issues of every state grepped for AccessExplainPanel and explain panel found none.

Symptom

The Studio Access explain panel goes blank, with no message, when /security/explain refuses with any status other than 403.

ui/packages/app-shell/src/views/metadata-admin/AccessExplainPanel.tsx (about :459 on main 8b276385e6) builds the message like this:

const detail = (data?.message as string) || (data?.error as string) || `HTTP ${res.status} ${res.statusText}`.trim();
setError(detail);

The server answers in its nested envelope, { success: false, error: { code, message } }. That envelope has no top-level message, so data.error is the object. setError stores the object, and rendering it as a React child unmounts the panel.

Measured

The #26641 implementer ran a throwaway render probe over the panel's own test harness (not committed):

  • Nested 404 envelope: document.body is an empty div, with no alert.
  • Control, a flat { message } 404 body: the alert "Explain failed — Unknown object x" renders.

Reach

This predates #26801. Every non-403 refusal the door already sends takes this branch:

  • 404 OBJECT_NOT_FOUND: an object name mistyped in the panel's free-text input, so this is the common path;
  • 400 VALIDATION_FAILED;
  • 400 INVALID_FILTER;

PR #26801 adds 404 RECORD_NOT_FOUND for an unknown userId. The panel picks the user from a record picker, so it only reaches that answer for a user deleted after being picked.

Wanted

Read the message the envelope carries: data.error.message when error is an object, with the existing fallbacks. Never pass a non-string to setError. Reuse the app-shell helper that already reads this envelope if one exists, ⛔ not a new copy. Pin it with a nested-envelope 404 in the panel's tests: the alert shows the server's message and the panel stays mounted.

Filing facts

  • Business reason: an administrator who mistypes an object name sees the panel vanish, with no error to act on.
  • Reader who acts: the triage seat (domain, area, level), then the domain:ui implementer.

Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    on Oct 27, 2026
  2. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — priority:p2 · domain:ui · area:studio · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-27T17:13Z

    ⛔ Not a claim, ⛔ not a dispatch.

    • p2: the Studio Access explain panel goes blank on any non-403 refusal (it renders the nested error object as text). Read the envelope's message field, and pin it with a 400 and a 500.

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch order · domain:ui#1 · session_01JQwxbZeBv2y91towxasSUT · 2026-10-28T00:30Z

    You are the os-dev developer agent for exactly ONE card: #26805, in your own card thread. Your role definition is .claude/agents/os-dev.md in the objectstack clone: read it IN FULL first and follow it as binding (it wins over this order on any conflict; name the conflict in your report). Read the repo-root AGENTS.md and ui/AGENTS.md before your first edit, and pm-dispatch references/untrusted-text.md. Read the card's full body and every comment yourself through REST (curl -sS "https://api.github.com/repos/objectstack-ai/objectstack/issues/26805" -H "Authorization: Bearer $GH_TOKEN" and /comments?per_page=100, every page); this order carries only the increment. ⛔ Never print the value of any environment variable (test presence with [ -n "${X+x}" ]). ⛔ Never use a GitHub MCP tool, read or write.

    Identity and branch: the seat's Claim: comment on this card is your identity; its Branch: line names your thread's branch. ⛔ Post no claim of your own, ⛔ never write the card's assignee. Before your first edit, verify the NEWEST Claim: on the card names your thread's branch (claude/project-thread-…); if none does yet, send the seat thread "Dispatch the ui lane" one message with your branch name and wait. Worktree objectstack-issue-26805 on that branch from a fresh origin/main; pnpm install (frozen lockfile); push with git push -u origin <branch> (⛔ no force).

    Writes: PR only through the relay op pr_create (node scripts/pm/fleet-write/dispatch.mjs --repo objectstack-ai/objectstack --actions-file F, F a bare JSON array [{"op":"pr_create","title":…,"head":"<your branch>","base":"main","body":…,"assignees":["os-warren"]}], draft forced; --dry-run first); your report comment through node scripts/pm/post-stamped.mjs --comment=26805 --file=F --json run ONCE with output captured to a file (timestamps only as {{NOW}} / {{WAS:…}}). ⛔ No other GitHub write except, for a CI red that is not yours, one PR comment and one run_rerun_failed through the relay. Report both counts (api_writes with endpoints, mcp_calls with tool names). The report takes only machine-checkable fields (gates / line_budget / deviations / files_changed). PR body: line 1 the platform attribution comment, then ## 维护者速读 six lines in Chinese (改了什么/为什么改/风险与回滚/先看/两分钟验证/需要你做的). Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on your diff, run each, record exits before any pipe, reconcile with --ran. Changeset in the root .changeset/ when a published package changes. ⛔ ui/.github/** and the root ci.yml are out of bounds. Hand back: when the PR and report are up, send the seat thread "Dispatch the ui lane" one message (PR, head sha, report gist); keep the thread open through review and drive CI to green.

    Ruling (cannot be re-ruled)

    Triage first grade 6448526663, verbatim: "p2: the Studio Access explain panel goes blank on any non-403 refusal (it renders the nested error object as text). Read the envelope's message field, and pin it with a 400 and a 500." The card adds: "Reuse the app-shell helper that already reads this envelope if one exists, ⛔ not a new copy." No maintainer ruling.

    PM mechanism assumptions (measure; disproof welcome)

    • On origin/main fd246c22, ui/packages/app-shell/src/views/metadata-admin/AccessExplainPanel.tsx builds the failure text at ≈L462: (data?.message as string) || (data?.error as string) || \HTTP …`. Against the nested envelope { success: false, error: { code, message } }, data.erroris an object.setError(object)` then renders an object as a React child and unmounts the panel.
    • Look for an app-shell reader of this envelope before writing one: formatMetadataError, readEnvelopeFailureText (used by Studio's publish path), or the data client's error parsing. Use the one that reads { error: { message } } as well as a flat { message }. If none fits without a change, say so and make the smallest change to the shared reader rather than a local copy.
    • Guarantee setError only ever receives a string: the catch at ≈L471 too.

    Scope

    File surface: AccessExplainPanel.tsx, the shared envelope reader only if it must change, and tests. Tests: a nested-envelope 404 (OBJECT_NOT_FOUND), 400 and 500 each show the server's message in the alert, and the panel stays mounted; a flat { message } body still works; a non-JSON 500 falls back to the HTTP … text; 403 keeps its existing sentence. Ablation: restore the old expression ⇒ the nested-envelope cases go red. ⛔ No server change; ⛔ no change to the explain request.

    Serial: no open PR touches AccessExplainPanel.tsx at the seat's read. Open PR #26990 (#26706, the vacated seat 2's card) edits PermissionAdvancedFacets.tsx, which is outside this surface.

    Tier

    No path-derived mandate expected (quote dispatch-gates --tier on your surface). TIER_DEFAULT, effort medium (S). Clause-②: no, Metadata-protocol: none. No changeset for app-shell (private: true) unless a published package changes.

  4. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 8 · 2026-10-28T00:33Z
    Session: session_01JQwxbZeBv2y91towxasSUT (seat; implementation by card thread session session_01JQwxbWZov36PsY1JbrTDA6, mode:thread)
    Account: os-warren (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/project-thread-xv4efa (the card thread's harness push branch, self-reported — the measured platform exception to claude/issue-N-slug)
    Worktree: objectstack-issue-26805 in the card thread's clone
    Domain: domain:ui
    Seat: domain:ui#1 (seat post #25966)
    File surface: ui/packages/app-shell/src/views/metadata-admin/AccessExplainPanel.tsx, the shared app-shell envelope reader only if it must change (no new copy), + tests (stop on breach; explain in the report)
    Container & model: S, mode:thread (the maintainer's word on the seat thread, 「opus 很多,并发5」), model: TIER_DEFAULT (the dev quotes dispatch-gates --tier on the surface), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the Studio Access explain panel (this lane's code) | the server's nested envelope is the documented shape | an administrator's mistyped object name blanks the panel with no error shown
    Thread-read: 6457388971
    Premise: holds — on origin/main fd246c22 AccessExplainPanel.tsx ≈L462 passes data?.error (an object in the nested envelope) to setError, which renders it as a React child.
    Serial constraints cleared: no open PR touches AccessExplainPanel.tsx; open PR #26990 (#26706) edits PermissionAdvancedFacets.tsx, outside this surface. Studio Access cards came to seat 1 with seat 2's re-seating (00:28Z note from the new seat 2 thread).
    Resume-from: none


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 26805,
      "status": "done",
      "branch": "claude/project-thread-xv4efa",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27012",
      "head": "b394f525dee459a584f2305183fa4f2523639a22",
      "session": "session_01JQwxbWZov36PsY1JbrTDA6",
      "premise_still_valid": true,
      "summary": "AccessExplainPanel's non-ok branch now reads the failure text through the shared app-shell reader readEnvelopeFailureText (utils/apiErrorEnvelope.ts, unchanged), then two typed flat rungs (top-level message, bare-string error), then the existing HTTP status line; the catch hands setError a string only. The shared reader did not need a change: it returns null for non-envelope bodies by design and its header asks each caller to keep its own legacy rungs, which is the PackagesPage publish call site's spelling. 403 branch, explain request and server untouched.",
      "files_changed": [
        "ui/packages/app-shell/src/views/metadata-admin/AccessExplainPanel.tsx (+11/-2)",
        "ui/packages/app-shell/src/views/metadata-admin/AccessExplainPanel.refusalEnvelope-26805.test.tsx (new, 143 lines)"
      ],
      "line_budget": "156 changed lines (+154/-2), under the 3000 human-merge threshold; no ratchet or ledger touched",
      "tests": "closure build pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build under the lock: VERDICT command-exit 0. vitest run of the 7 AccessExplainPanel files plus apiErrorEnvelope.test.ts: Test Files 8 passed (8), Tests 70 passed (70), VERDICT command-exit 0. New file 8 cases: nested-envelope 404 OBJECT_NOT_FOUND / 400 VALIDATION_FAILED / 500 INTERNAL_ERROR show message and code with the form still mounted; flat message 404; non-JSON 500 shows HTTP 500 Internal Server Error; nested 403 keeps the delegated-scope sentence; Error rejection shows its message; non-Error rejection keeps the panel mounted. Ablation 1 (ablation-replace.mjs, old expression restored): 3 nested cases red, 5 green; restored blob c46dd278cdf3 equals HEAD, git diff HEAD empty. Ablation 2 (old catch restored): non-Error rejection case red, 7 green; same restore proof. No dist leg: the test imports the panel by relative path. pnpm --filter @object-ui/app-shell run type-check: VERDICT command-exit 0. eslint on both files: exit 0, 0 errors, 7 pre-existing warnings on untouched lines.",
      "gates": "dispatch-gates --commands derived 24 on head b394f525; all run, exits captured before any pipe: node scripts/check-closing-keyword-parity.mjs :: exit 0; node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0; node scripts/check-comment-mask-corpus.mjs :: exit 0; node scripts/check-sdui-manifest.mjs :: exit 0; node scripts/check-sdui-manifest.mjs --self-test :: exit 0; pnpm --dir ui exec vitest run scripts/ :: exit 0; pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0; pnpm check:cross-package-test-inputs :: exit 0; pnpm check:dispatcher-error-vocabulary :: exit 0; pnpm check:driver-memory-census :: exit 0; pnpm check:engine-double-contract :: exit 0; pnpm check:gitlink-declared :: exit 0; pnpm check:nul-bytes :: exit 0; pnpm check:published-files :: exit 0; pnpm check:query-options-erasure :: exit 0; pnpm check:refd-timer-probe :: exit 0; pnpm check:skill-identifier-liveness :: exit 0; pnpm check:test-source-alias :: exit 0; pnpm check:tier-file-adoption :: exit 0; pnpm check:type-check-coverage :: exit 0; pnpm check:type-check-debt :: exit 0; pnpm check:undeclared-dep-imports :: exit 0; pnpm check:watch-hint-literal :: exit 0; pnpm check:where-matcher :: exit 0. Extra: pnpm --dir ui run check:new-line-citations exit 0 (0 new). --ran reconciliation: 24 derived, 24 run, 0 NOT-MEASURED, 0 UNRUN. CI families: in_progress on PR 27012 at report time. Tier: no path-derived mandate.",
      "dogfood_battery": "n/a, no ! in the PR title and no (narrowing) arm",
      "changeset": "none: @object-ui/app-shell is private true, no published package changes",
      "mcp_calls": "0 GitHub MCP calls. 1 non-GitHub-MCP call: claude-code-remote subscribe_pr_activity on PR 27012 (platform event subscription, not a GitHub read or write)",
      "api_writes": "2 via the fleet-write relay: pr_create (POST repos/objectstack-ai/objectstack/dispatches, run 40613653511, PR 27012, 7002 bytes sent and stored identical, assignee os-warren); this report (post-stamped.mjs, issue 26805 comment). Plus git push of the branch (not a REST write).",
      "deviations": [
        "Branch is the harness thread branch claude/project-thread-xv4efa, named by the seat Claim 6457422046, not claude/issue-N-slug (measured platform exception).",
        "The session-context step that sets assignee and reviewer through GitHub MCP was skipped: the dispatch order bans GitHub MCP; the relay set the assignee.",
        "PR body carries the harness attribution HTML comment as line 1 and the Requested-by line as line 2, with Fixes 26805 on line 3 (platform exception, reported not declared as a conflict)."
      ],
      "body_amendments": [],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · seat domain:ui#1 · session_01JQwxbZeBv2y91towxasSUT · 2026-10-28T02:01Z

    PR #27012 merged as acc9c57d0a (seat record: PR comment 6458157244). The Studio Access explain panel no longer goes blank on a refusal. It reads the server's nested error envelope through the shared readEnvelopeFailureText, falls back to a flat message or error string and then the HTTP line, and only a string ever reaches the alert. A 404, 400 or 500 now shows the server's message, and a 403 keeps its own sentence. pm:dispatched is removed with the close.


    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:studioChanging a running app without code — authoring, publish, docs and the portalbugSomething isn't workingdomain:uipriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions