Skip to content

fix(react,components,app-shell): a delegated admin's out-of-scope save shows the server's reason - #26363

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-47phpr
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-47phpr

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #26190

Clause-②: yes (widening: @object-ui/react exports a new delegationScopeMessage)
Metadata-protocol: none

维护者速读

What changes

  • @object-ui/react: new exported delegationScopeMessage(err). It answers the server's reason, without the [Security] machine prefix (via extractWriteErrorMessage), when the error's code is DELEGATION_SCOPE_VIOLATION (read from code, then details.code, the same pair isPermissionError reads, case-insensitive through errorCodeIs). It answers null for everything else, and for a scope refusal with no usable text. isPermissionError is unchanged and still answers true for any 403; its doc now says to ask the new reader first.
  • @object-ui/components (record form): the message chain is declaredUserMessage → delegationScopeMessage → isPermissionError ? form.noPermissionToSave : server text. The toast and the in-form banner share the value.
  • @object-ui/app-shell (flow run path, flowFailureMessage): the inner refusal is asked for the scope reason before the permission check, so a write node that met the same refusal shows the same reason.
  • Root changeset .changeset/26190-delegation-scope-refusal-reason.md (patch on the three packages).
  • No sdui record. The per-package records were retired by ci(sdui): retire the per-package SDUI manifest records; CI's regeneration holds freshness (#26137 PR B) #26367 while this PR was open. The main merge took the modify/delete with git rm, and CI's Build & E2E UI regenerates the manifest.

Tests

  • ui/packages/react/src/utils/error-message.test.ts: delegationScopeMessage answers the reason for the client's 403 shape and for details.code; answers null for PERMISSION_DENIED, FORBIDDEN, a bare 403, a row-level-security message and null; answers null for a scope refusal with no text. One assertion pins why it must be asked first: isPermissionError answers true for the same error.
  • ui/packages/components/src/__tests__/form-write-error-message.test.tsx: a scope refusal renders the reason without [Security] and without the fixed line; CONTROL: a plain 403 PERMISSION_DENIED keeps the fixed line and withholds its text.
  • ui/packages/app-shell/src/utils/__tests__/flowResponse.test.ts: the 400 FLOW_FAILED door and the 200 failed arm both show the reason; CONTROL on the same path: a PERMISSION_DENIED inner refusal still reads form.noPermissionToSave.
  • No forced edits to existing tests.

Acceptance notes

  • Flow site premise, measured. On the flow path the inner refusal carries code and message but no status, and the scope reason does not match isPermissionError's text pattern, so before this change the flow path already showed the server text, with its [Security] prefix. The ablation shows exactly that: with the new line removed the test fails on the prefix, not on the fixed line. The explicit read makes the rule hold by code rather than by the text happening not to match, and makes the two sites render the same string.
  • The card's note about the producer-side userMessage marking: not used. The ruled shape is the code; userMessage still wins when a producer sets it, as before.
  • delegationScopeMessage is a new export of @object-ui/react, so the declaration is Clause-②: yes (widening), re-declared at the seat's review (the claim first said no); the changeset carries the same line. Metadata-protocol stays none and the bump stays patch.

Verification

Head: 2f86b3a3 (code); c786eccb changes only the changeset's Clause-② line, and the changeset gates (check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check:changeset-gate-self-tests) exit 0 there

Tests (lock VERDICT lines)

All runs on head 2f86b3a3 (origin/main 222755eb merged in), under scripts/pm/os-verify-lock.sh:

pnpm --dir ui exec vitest run --maxWorkers=2 packages/react/src/utils/ packages/components/src/__tests__/form-write-error-message.test.tsx packages/app-shell/src/utils/__tests__/flowResponse.test.ts
 Test Files  8 passed (8)
      Tests  145 passed (145)
pnpm --filter @object-ui/react run type-check       (tsc --noEmit && tsc -p tsconfig.test.json)
pnpm --filter @object-ui/components run type-check
pnpm --filter @object-ui/app-shell run type-check
os-verify-lock: VERDICT command-exit 0
Ablations

Taken on head 43106bd8 (pre-merge, before the record commits) with scripts/ablation-replace.mjs; tests resolve @object-ui/* to src/ through the vitest alias, so no build sits between the mutation and the run.

form.tsx: delete the line "delegationScopeMessage(error) ??"
  ok mutation landed: anchor 1 -> 0
  x renders the server's reason, without the machine prefix, instead of the fixed line
  Tests  1 failed | 10 passed (11)
  ok restored: blob == HEAD and `git diff HEAD` is empty
flowResponse.ts: delete the line "if (scopeReason) return scopeReason;"
  ok mutation landed: anchor 1 -> 0
  x a delegated-admin scope refusal shows the server reason, not the fixed permission line
  - "error": "Outside your delegated scope: ..."
  + "error": "[Security] Outside your delegated scope: ..."
  Tests  1 failed | 64 passed (65)
  ok restored: blob == HEAD and `git diff HEAD` is empty
Gates (dispatch-gates --commands, 35 commands)

Derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 2f86b3a3; every command run there; reconciled:

Run reconciliation — 45 derived, 45 run, 0 NOT-MEASURED, 0 UNRUN.
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-changeset-no-major.mjs --self-test :: exit 0
node scripts/check-ci-filter-parity.mjs :: exit 0
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-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-scripts-symbol-anchors.mjs :: exit 0
node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/check-undeclared-dep-imports.mjs :: exit 0
node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:closed-ceremony-text :: 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:entry-guard :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:parse-guard :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:pnpm-filter-targets :: exit 0
pnpm check:pnpm-flag-spellings :: 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:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

CI owns the type-check lanes and the repo-wide scans.


Generated by Claude Code

…e shows the server's reason

A 403 DELEGATION_SCOPE_VIOLATION carries the boundary of the caller's own
delegated scope that refused the write. The record form and the flow-run
path classed every 403 as a plain denial and showed the fixed
form.noPermissionToSave line. delegationScopeMessage reads the code before
isPermissionError; every other denial keeps the fixed line (ruling B' on
the record-save rule, unchanged).

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZRTRVa5FXXjuAJC6ZPM8P
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 27, 2026
…ause-② widening

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZRTRVa5FXXjuAJC6ZPM8P
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · seat domain:ui#1 · 2026-10-27T02:15Z

Head 25de53bc (read from the PR object). It merges main after #26367 retired the per-package sdui records. The only conflicts were modify/delete on react.json / components.json, taken with git rm.

Landing: ready + auto-merge through the relay.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 02:17
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 02:17
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 0e7be7e Oct 27, 2026
61 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-47phpr branch October 27, 2026 02:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Record form toast: a delegated-admin scope refusal shows the fixed no-permission line instead of the server's reason

2 participants