Repository navigation
fix(examples): showcase record write actions write as the caller instead of through an elevated script body - #25656
Merged
Conversation
…ead of through an elevated script body Mark Done, Submit for Sign-off and Reopen were script actions whose body wrote through ctx.api, which runs system-elevated, so any reader of a task or invoice could change it through the button. They are now declared writes (operation: 'update' + patch): the platform action route performs the write as the caller, so the edit grant, row and field security, hooks and validations apply exactly as for the Edit button. Claude-Session: https://claude.ai/code/session_01SGzB5Np4fydLKpGjZm1dNr Co-authored-by: Claude <noreply@anthropic.com>
…own write verdict Claude-Session: https://claude.ai/code/session_01SGzB5Np4fydLKpGjZm1dNr Co-authored-by: Claude <noreply@anthropic.com>
…declares its write Claude-Session: https://claude.ai/code/session_01SGzB5Np4fydLKpGjZm1dNr Co-authored-by: Claude <noreply@anthropic.com>
… so the disabled-dispatch item no longer says it cannot Claude-Session: https://claude.ai/code/session_01SGzB5Np4fydLKpGjZm1dNr Co-authored-by: Claude <noreply@anthropic.com>
…-z0wf71 Claude-Session: https://claude.ai/code/session_01SGzB5Np4fydLKpGjZm1dNr Co-authored-by: Claude <noreply@anthropic.com>
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Contributor
Author
席位核对 · ACCEPT · 2026-10-24T13:49Z
Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Sales · project thread
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 whosectx.apiruns with the system identity (buildActionExecutionContextstampsisSystem: 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 aPATCHof the same row. A caller who may edit the record uses the button as before; a caller who may not gets the same403the 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 holdsshowcase_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.tsshowcase_mark_done:patch: { done: true, progress: 100 },execution: 'perRecord'; body removed.showcase_reopen_task:patch: { done: false },execution: 'perRecord'; body removed. The guard hookshowcase_guard_task_reopenstill refuses the reopening write with its sentence — now it refuses the caller's own update.showcase_submit_signoff:patch: { status: 'sent' }; body removed.successMessage,visible,locationsandrefreshAfterare unchanged.typestays'script'(the schema default for a declared write).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 fix(showcase): register Mark Done action handler via inline body #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:devxtouch, declared)Two items named these actions' script bodies and are respelled, with
revisionbumped and ahistoryrow 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 id404 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]andacceptance[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 answers400 VALIDATION_ERRORcarryingTASK_REOPEN_REFUSAL. The console dispatches anoperation: 'update'action through the same registered script handler (objectuiActionRunner.executeUpdateOperationat the.objectui-shapin6fb31dc), so the toast chain the item observes is the same.pnpm check:platform-checklistis green onb829a362.Tests
All on the merged head
b829a362(this branch plusorigin/main49653f91), unless noted:pnpm --filter @objectstack/example-showcase test: 39 files, 484 tests passed;tsc --noEmitexit 0.packages/qa/dogfood/test/showcase-record-write-actions.dogfood.test.ts: 7/7 passed (run underscripts/pm/os-verify-lock.sh); dogfoodtsc --noEmitexit 0.b829a362): withexamples/app-showcase/src/ui/actions/index.tsrestored fromorigin/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 withgit checkout HEAD -- path, andgit status --porcelainwas empty.eslint --no-inline-configon 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 --commandsderived 63 commands for this file surface; all 63 ran with exit 0, and--ranreconciles 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) andcheck: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
@objectstack/example-showcaseisprivate: trueand publishes nothing (skip-changeset).claude/project-thread-z0wf71is the harness-pinned branch for this session and names no issue; the claim comment 6372266664 on app-showcase: "Mark Done" lets a member who may not edit tasks write them — the script body runs elevated, the action declares no requiredPermissions, and the console still offers the button #25468 is the identity record (services seat 1,session_01SGzB5Np4fydLKpGjZm1dNr).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 a403toast when they click it.Acceptance notes
showcase_portfolio_snapshotkeeps its body. It reads (capabilities: ['api.read']), isai.exposed, and declares norequiredPermissions, 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.api-backend.packaged-action-disabled-dispatchdescribed above predates this change; it is fixed here because this PR edits that item.🤖 Generated with Claude Code
https://claude.ai/code/session_01SGzB5Np4fydLKpGjZm1dNr
Generated by Claude Code