Skip to content

fix(examples): showcase record write actions write as the caller instead of through an elevated script body - #25656

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-z0wf71
Oct 24, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-z0wf71

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 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 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: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

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.ai/code/session_01SGzB5Np4fydLKpGjZm1dNr


Generated by Claude Code

…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>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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): node scripts/docs-audit/affected-docs.mjs --json 49653f91812815c72eb9194bf0a6cc451cf711f2 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · 2026-10-24T13:49Z

domain:server seat 1 (seat post #6367) · os-sales · session_01JrXUZYyTV4i588KLZRxSvJ · card #25468 (claim 6372266664, ruling B 6384686314, os-dev-report 6384783002) · reviewed head b829a36273 · merge base 49653f91

  • Shape: showcase_mark_done, showcase_reopen_task and showcase_submit_signoff drop their ctx.api write bodies and declare operation: 'update' + patch ({ done: true, progress: 100 }, { done: false }, { status: 'sent' }). Labels, icons, visible, locations, successMessage, refreshAfter and execution are unchanged. 8 files, +378 / −147: the actions, two comment-only showcase files, the two showcase test files, two platform-checklist items (a declared domain:devx touch), and one new dogfood pin. This is ruling B as recorded; the departure from the triage grading is the maintainer's ruling, not the seat's.
  • Premise, read by the seat in the cited code: the action door takes a declared update before the type switch and before the elevated body context is built, and executeDeclarativeUpdateAction (packages/runtime/src/action-execution.ts) writes once through callData('update', …, ec) with the caller's own envelope, after refuseDeniedSubjectLoad. So the write carries the caller's object, row and field permissions, hooks and validations, as a PATCH does. visible stays a display rule, not a gate, as before.
  • Gates and tests, re-run by the seat in its own worktree at b829a36273:
    • @objectstack/example-showcase vitest: 39 files, 484 passed.
    • showcase-record-write-actions.dogfood.test.ts over real HTTP (dogfood closure built): 7 / 7 passed.
    • Ablation: examples/app-showcase/src/ui/actions/index.ts restored from origin/main (bodies back). Showcase vitest: exactly 3 red (Mark Done's declared completion write, the "no action body writes without a capability gate" pin, Reopen's declared shape). Dogfood: exactly the 3 refusal pins red (Mark Done, Reopen, Submit for Sign-off each answering other than the data plane's 403), the 4 allow pins green. File restored, git status --porcelain empty.
    • The dev's dispatch-gates --commands reconciliation (63 / 63, 0 NOT-MEASURED) is taken as reported; not re-run.
  • Check-runs on the head: 47 runs, 35 success and 12 expected skips, none failed or pending.
  • Contract review: not owed. The diff touches no published schema and no governed path, declares Clause-②: no with no narrowing arm, and the package is private: true (skip-changeset). The seat's own read of the diff is the review of record. check-governed-merges --pr 25656: not governed, 525 changed lines.
  • Accepted deviations: the checklist item api-backend.packaged-action-disabled-dispatch loses a sentence that was already false before this change (the enabled control on a missing record answers 404 RECORD_NOT_FOUND); respelled with a revision bump and history row, accepted.
  • Out of scope, disposition: the PR's acceptance note on showcase_portfolio_snapshot (a read-only body that computes counts under the system identity for any signed-in caller). It writes nothing and the grading took write actions only, so it does not block this card. This term files no new cards; the seat reports it to the maintainer through the project for a call. No card is filed here.
  • Disclosure: the PR body and this record describe mechanism only.
  • Mergeable: git merge-tree onto today's main is clean.
  • Landing: Tier S, not governed. The seat readies and arms the PR once the maintainer's go-ahead for this term's landings is in hand; the reviewer request for os-sales is added in this act.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot requested a review from os-sales October 24, 2026 13:50
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 24, 2026 14:17
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 24, 2026 14:18
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit 1286eea Oct 24, 2026
48 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-z0wf71 branch October 24, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants