Follow-on from #13653, which added hook-api-update-readonly-field for hook bodies. The action body surface has the identical hole and is not covered.
The gap
An action body reaches the engine through the same ScopedContext, so ctx.api.object('x').update({ someReadonlyField }) in an action is dropped by the same non-system stripReadonlyFields pass, with the same silence: the call returns success and the column stays null.
validate-action-body-writes.ts already parses every action body with the same extractor and already resolves field names against the target object. It answers "does this field exist?" but never "is it writable through this channel?" — exactly the split that existed on the hook side before #13653.
Why this is worth filing rather than assuming it is theoretical
The population is live, measured on origin/main at 787d75740 by running the real extractHookBodyWrites over every source: literal in examples/ (187 body sources parsed, positive control: 4 ctx.input writes seen):
examples/app-showcase/src/ui/actions/index.ts :: showcase_task.id via update
examples/app-showcase/src/ui/actions/index.ts :: showcase_task.done via update
examples/app-showcase/src/ui/actions/index.ts :: showcase_task.progress via update
examples/app-showcase/src/ui/actions/index.ts :: showcase_invoice.id via update
examples/app-showcase/src/ui/actions/index.ts :: showcase_invoice.status via update
Five ctx.api update writes in the shipped showcase app. None currently targets a readonly field (done, progress and both status fields are all writable today), so there is no live outage — but there is also nothing stopping the next author adding readonly: to one of them, which is precisely the prose-only protection #13653 was filed about.
Scope note
Deliberately NOT folded into the #13653 PR: the ruling on that card narrowed it to the hook side, and the action surface is a separate write surface with its own rule file, its own pattern-subset ledger and its own test suite. Widening it there would have been undeclared scope.
Shape of the fix, if accepted
validate-readonly-hook-writes.ts is already channel-keyed and reuses buildReadonlyIndex from the flow rule. The action version is the same judgement with one difference worth checking before coding: an action's ctx.record is a dead snapshot, and validate-action-body-writes.ts already tracks ctxRecordEscapes to tell a live write from a discarded one — so the readonly question only applies to the ctx.api shapes there too, not to ctx.record.
Please confirm the severity should match the hook side (error for static readonly, warning for readonlyWhen) rather than being re-argued.
Generated by Claude Code
Generated by Claude Code
Follow-on from #13653, which added
hook-api-update-readonly-fieldfor hook bodies. The action body surface has the identical hole and is not covered.The gap
An action body reaches the engine through the same
ScopedContext, soctx.api.object('x').update({ someReadonlyField })in an action is dropped by the same non-systemstripReadonlyFieldspass, with the same silence: the call returns success and the column stays null.validate-action-body-writes.tsalready parses every action body with the same extractor and already resolves field names against the target object. It answers "does this field exist?" but never "is it writable through this channel?" — exactly the split that existed on the hook side before #13653.Why this is worth filing rather than assuming it is theoretical
The population is live, measured on
origin/mainat787d75740by running the realextractHookBodyWritesover everysource:literal inexamples/(187 body sources parsed, positive control: 4ctx.inputwrites seen):Five
ctx.apiupdate writes in the shipped showcase app. None currently targets areadonlyfield (done,progressand bothstatusfields are all writable today), so there is no live outage — but there is also nothing stopping the next author addingreadonly:to one of them, which is precisely the prose-only protection #13653 was filed about.Scope note
Deliberately NOT folded into the #13653 PR: the ruling on that card narrowed it to the hook side, and the action surface is a separate write surface with its own rule file, its own pattern-subset ledger and its own test suite. Widening it there would have been undeclared scope.
Shape of the fix, if accepted
validate-readonly-hook-writes.tsis already channel-keyed and reusesbuildReadonlyIndexfrom the flow rule. The action version is the same judgement with one difference worth checking before coding: an action'sctx.recordis a dead snapshot, andvalidate-action-body-writes.tsalready tracksctxRecordEscapesto tell a live write from a discarded one — so the readonly question only applies to thectx.apishapes there too, not toctx.record.Please confirm the severity should match the hook side (
errorfor staticreadonly,warningforreadonlyWhen) rather than being re-argued.Generated by Claude Code
Generated by Claude Code