Repository navigation
Commit 1286eea
fix(examples): showcase record write actions write as the caller instead of through an elevated script body (#25656)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4Uu7ZZBnHrYcv5pDjzwJhi4K3)_
Fixes #25468
## Summary
Three showcase record actions — **Mark Done** (`showcase_mark_done`),
**Reopen** (`showcase_reopen_task`) and **Submit for Sign-off**
(`showcase_submit_signoff`) — set fields on their own record and nothing
else, but they did it from a script body whose `ctx.api` runs with the
system identity (`buildActionExecutionContext` stamps `isSystem: true`).
So the write did not carry the caller's object, row or field
permissions.
Each one now **declares** the write (`operation: 'update'` + `patch`,
spec #14092, server #15079), and the platform action route performs it
**as the caller**. Object CRUD, RLS, FLS, hooks and validation all
apply, exactly as for a `PATCH` of the same row. A caller who may edit
the record uses the button as before; a caller who may not gets the same
`403` the data plane gives, and nothing is written.
**Ruled by Sales: B** (ruling record 6384686314 on #25468, Sales
verbatim 「25468 B」). This departs from the triage grading 6369139790,
which prescribed `requiredPermissions` (ADR-0066 D4); the departure is
Sales's ruling. Under that route the three actions would have been
usable by nobody on a fresh boot, admins included: admins hold no
package capability, neither gate has an admin bypass, and no provisioned
showcase user holds `showcase_contributor`. The declared write itself is
the maintainer's 2026-09-01 ruling on #14092 (5494341350, 「同意」), with
#14010's 5494343943: the write runs as the caller, never
system-elevated.
## What changed
- `examples/app-showcase/src/ui/actions/index.ts`
- `showcase_mark_done`: `patch: { done: true, progress: 100 }`,
`execution: 'perRecord'`; body removed.
- `showcase_reopen_task`: `patch: { done: false }`, `execution:
'perRecord'`; body removed. The guard hook `showcase_guard_task_reopen`
still refuses the reopening write with its sentence — now it refuses the
caller's own update.
- `showcase_submit_signoff`: `patch: { status: 'sent' }`; body removed.
- Labels, icons, `successMessage`, `visible`, `locations` and
`refreshAfter` are unchanged. `type` stays `'script'` (the schema
default for a declared write).
- The docblocks say which rule the showcase models: an action that only
sets fields on its record declares the write; a script body that writes
gates itself with `requiredPermissions` (ADR-0066 D4).
- `examples/app-showcase/src/data/hooks/index.ts`,
`src/ui/views/task.view.ts`: comments only.
- `examples/app-showcase/test/actions.test.ts`: the executability sweep
accepts a declared write; the #2169 handler regression now iterates the
actions that still have a body; a new pin asserts that **no action body
writes without a capability gate**, and that the declared writes are
exactly these three.
- `examples/app-showcase/test/task-reopen-hook-refusal.test.ts`: half 2
now pins the declared write (`operation: 'update'`, `patch: { done:
false }`, no body).
-
`packages/qa/dogfood/test/showcase-record-write-actions.dogfood.test.ts`
(new): over real HTTP on the stock showcase, each refusal is paired with
the data plane's own answer for the same caller and row, and with an
allow for a caller who may edit.
## Platform checklist (a `domain:devx` touch, declared)
Two items named these actions' script bodies and are respelled, with
`revision` bumped and a `history` row each:
- `api-backend.packaged-action-disabled-dispatch` (rev 7): Mark Done is
described as a declared write. While measuring it, the same boot
answered the **enabled** control on a nonexistent record id `404
RECORD_NOT_FOUND`, which falsified the item's "the enabled control
CANNOT 404". Every door already consumes the caller-scope load's verdict
(`refuseDeniedSubjectLoad`), the script door included, so the text was
stale before this change too. `knownGaps[1]` and `acceptance[2]` are
respelled; `acceptance[2]` still scores from the disabled traces alone.
- `records-forms.script-action-hook-refusal-toast` (rev 3): Reopen is
described as a declared write. The two source rows for the nested
action-VM hop leave, replaced by the route's catch and the new dogfood
pin. The observable answer is unchanged: measured, the admin's Reopen
still answers `400 VALIDATION_ERROR` carrying `TASK_REOPEN_REFUSAL`. The
console dispatches an `operation: 'update'` action through the same
registered script handler (objectui
`ActionRunner.executeUpdateOperation` at the `.objectui-sha` pin
`6fb31dc`), so the toast chain the item observes is the same.
`pnpm check:platform-checklist` is green on `b829a362`.
## Tests
All on the merged head `b829a362` (this branch plus `origin/main`
`49653f91`), unless noted:
- `pnpm --filter @objectstack/example-showcase test`: **39 files, 484
tests passed**; `tsc --noEmit` exit 0.
-
`packages/qa/dogfood/test/showcase-record-write-actions.dogfood.test.ts`:
**7/7 passed** (run under `scripts/pm/os-verify-lock.sh`); dogfood `tsc
--noEmit` exit 0.
- **Ablation** (run on the pre-merge head and again on `b829a362`): with
`examples/app-showcase/src/ui/actions/index.ts` restored from
`origin/main` (bodies back), exactly the **3 refusal pins go red** (the
action route no longer answers as the data plane does) and the 4 allow
pins stay green. The file was then restored with `git checkout HEAD --
path`, and `git status --porcelain` was empty.
- `eslint --no-inline-config` on the six changed source/test files: exit
0.
## Gates
On the pre-merge head `3027e0a3` (the merge brings no change to this
diff's files), `node scripts/pm/dispatch-gates.mjs --commands` derived
63 commands for this file surface; all 63 ran with exit 0, and `--ran`
reconciles **63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN**. Two first
answered exit 3 (a prerequisite, not a verdict) and were rerun after it
was met: `check-plugin-teardown-shape.mjs --self-test` (its pinned
fixture commit was missing from the shallow clone; fetched) and
`check:dual-build-cjs-loads` (seven unbuilt dists; built). Dogfood
battery: n/a (no `!` in the title, no narrowing arm).
## Declarations
Clause-②: no
Metadata-protocol: none
- No changeset: `@objectstack/example-showcase` is `private: true` and
publishes nothing (`skip-changeset`).
- Branch `claude/project-thread-z0wf71` is the harness-pinned branch for
this session and names no issue; the claim comment 6372266664 on #25468
is the identity record (services seat 1,
`session_01SGzB5Np4fydLKpGjZm1dNr`).
- Not `packages/**` code, not objectui. A hint that hides the button
from a caller who may not edit is a console follow-up the triage
explicitly kept out; today such a caller still sees the button and gets
a `403` toast when they click it.
## Acceptance notes
- `showcase_portfolio_snapshot` keeps its body. It reads (`capabilities:
['api.read']`), is `ai.exposed`, and declares no `requiredPermissions`,
so its counts of accounts, projects and invoices are computed with the
system identity for any signed-in caller, including one who cannot read
invoices. It writes nothing, and it was outside this card's grading
(write actions only). Noted for the seat to judge; carrier: none.
- The record-load staleness in
`api-backend.packaged-action-disabled-dispatch` described above predates
this change; it is fixed here because this PR edits that item.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01SGzB5Np4fydLKpGjZm1dNr
---
_Generated by [Claude
Code](https://claude.ai/code/session_01SGzB5Np4fydLKpGjZm1dNr)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent f123f75 commit 1286eea
8 files changed
Lines changed: 378 additions & 147 deletions
File tree
- docs/qa/platform-checklist/areas
- examples/app-showcase
- src
- data/hooks
- ui
- actions
- views
- test
- packages/qa/dogfood/test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1607 | 1607 | | |
1608 | 1608 | | |
1609 | 1609 | | |
1610 | | - | |
| 1610 | + | |
1611 | 1611 | | |
1612 | 1612 | | |
1613 | 1613 | | |
| |||
1617 | 1617 | | |
1618 | 1618 | | |
1619 | 1619 | | |
1620 | | - | |
| 1620 | + | |
1621 | 1621 | | |
1622 | 1622 | | |
1623 | 1623 | | |
1624 | 1624 | | |
1625 | 1625 | | |
1626 | 1626 | | |
1627 | | - | |
| 1627 | + | |
1628 | 1628 | | |
1629 | 1629 | | |
1630 | 1630 | | |
| |||
1639 | 1639 | | |
1640 | 1640 | | |
1641 | 1641 | | |
1642 | | - | |
| 1642 | + | |
1643 | 1643 | | |
1644 | 1644 | | |
1645 | 1645 | | |
| |||
1651 | 1651 | | |
1652 | 1652 | | |
1653 | 1653 | | |
1654 | | - | |
| 1654 | + | |
1655 | 1655 | | |
1656 | | - | |
| 1656 | + | |
1657 | 1657 | | |
1658 | 1658 | | |
1659 | 1659 | | |
| |||
1748 | 1748 | | |
1749 | 1749 | | |
1750 | 1750 | | |
| 1751 | + | |
| 1752 | + | |
| 1753 | + | |
| 1754 | + | |
| 1755 | + | |
| 1756 | + | |
1751 | 1757 | | |
1752 | 1758 | | |
1753 | 1759 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5053 | 5053 | | |
5054 | 5054 | | |
5055 | 5055 | | |
5056 | | - | |
| 5056 | + | |
5057 | 5057 | | |
5058 | 5058 | | |
5059 | 5059 | | |
| |||
5062 | 5062 | | |
5063 | 5063 | | |
5064 | 5064 | | |
5065 | | - | |
| 5065 | + | |
5066 | 5066 | | |
5067 | 5067 | | |
5068 | 5068 | | |
| |||
5123 | 5123 | | |
5124 | 5124 | | |
5125 | 5125 | | |
5126 | | - | |
| 5126 | + | |
5127 | 5127 | | |
5128 | | - | |
5129 | | - | |
| 5128 | + | |
| 5129 | + | |
5130 | 5130 | | |
5131 | 5131 | | |
5132 | 5132 | | |
| |||
5143 | 5143 | | |
5144 | 5144 | | |
5145 | 5145 | | |
| 5146 | + | |
| 5147 | + | |
| 5148 | + | |
| 5149 | + | |
| 5150 | + | |
| 5151 | + | |
5146 | 5152 | | |
5147 | 5153 | | |
5148 | 5154 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
133 | 133 | | |
134 | 134 | | |
135 | 135 | | |
136 | | - | |
137 | | - | |
138 | | - | |
139 | | - | |
140 | | - | |
141 | | - | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
142 | 143 | | |
143 | 144 | | |
144 | 145 | | |
| |||
0 commit comments