Repository navigation
Commit 0e7be7e
fix(react,components,app-shell): a delegated admin's out-of-scope save shows the server's reason (#26363)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9SmSkP71JiVHXqMg5WDuVZY)_
Fixes #26190
Clause-②: yes (widening: `@object-ui/react` exports a new
`delegationScopeMessage`)
Metadata-protocol: none
## 维护者速读
- 改了什么:委派管理员在自己范围之外保存时(服务端回 `403 DELEGATION_SCOPE_VIOLATION`),记录表单的
toast 与表单横幅、以及 flow 运行失败提示,改为显示服务端给出的原因;其它所有拒绝照旧显示固定的「没有权限保存」。
- 为什么改:#26059 裁决 Q3 第 4
项。委派管理员需要知道是哪条边界(业务单元、权限集白名单、目标用户)拦下了操作,才能自己修正分配;原来所有 403 都被当成普通拒绝。
- 风险与回滚:只读这一个错误码,`PERMISSION_DENIED`、`FORBIDDEN`、裸
403、行级安全拒绝的分支不变(#24914 B′ 不变);回滚即 revert 本 PR。
- 先看:`ui/packages/react/src/utils/error-message.ts`(新函数
`delegationScopeMessage`)、`ui/packages/components/src/renderers/form/form.tsx`(错误文案取值链)、`ui/packages/app-shell/src/utils/flowResponse.ts`(`flowFailureMessage`)。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/react/src/utils/error-message.test.ts
packages/components/src/__tests__/form-write-error-message.test.tsx
packages/app-shell/src/utils/__tests__/flowResponse.test.ts`,预期 3
个文件全部通过。
- 需要你做的:无,席位落地。
## 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 #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
<details><summary>Tests (lock VERDICT lines)</summary>
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
```
</details>
<details><summary>Ablations</summary>
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
```
</details>
<details><summary>Gates (dispatch-gates --commands, 35
commands)</summary>
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.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JZRTRVa5FXXjuAJC6ZPM8P)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 92a0073 commit 0e7be7e
8 files changed
Lines changed: 188 additions & 5 deletions
File tree
- .changeset
- ui/packages
- app-shell/src/utils
- __tests__
- components/src
- __tests__
- renderers/form
- react/src
- utils
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
696 | 696 | | |
697 | 697 | | |
698 | 698 | | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
699 | 718 | | |
700 | 719 | | |
701 | 720 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
214 | 214 | | |
215 | 215 | | |
216 | 216 | | |
217 | | - | |
| 217 | + | |
218 | 218 | | |
219 | 219 | | |
220 | 220 | | |
| |||
368 | 368 | | |
369 | 369 | | |
370 | 370 | | |
371 | | - | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
372 | 374 | | |
373 | 375 | | |
374 | 376 | | |
| |||
378 | 380 | | |
379 | 381 | | |
380 | 382 | | |
| 383 | + | |
| 384 | + | |
381 | 385 | | |
382 | 386 | | |
383 | 387 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
160 | 160 | | |
161 | 161 | | |
162 | 162 | | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | | - | |
| 45 | + | |
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
| |||
3048 | 3048 | | |
3049 | 3049 | | |
3050 | 3050 | | |
| 3051 | + | |
| 3052 | + | |
| 3053 | + | |
| 3054 | + | |
| 3055 | + | |
| 3056 | + | |
3051 | 3057 | | |
3052 | 3058 | | |
| 3059 | + | |
3053 | 3060 | | |
3054 | 3061 | | |
3055 | 3062 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
87 | | - | |
| 87 | + | |
88 | 88 | | |
89 | 89 | | |
90 | 90 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
| 13 | + | |
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| |||
250 | 250 | | |
251 | 251 | | |
252 | 252 | | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| 25 | + | |
25 | 26 | | |
26 | 27 | | |
27 | 28 | | |
| |||
136 | 137 | | |
137 | 138 | | |
138 | 139 | | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
139 | 177 | | |
140 | 178 | | |
141 | 179 | | |
| |||
198 | 236 | | |
199 | 237 | | |
200 | 238 | | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
201 | 243 | | |
202 | 244 | | |
203 | 245 | | |
| |||
0 commit comments