Symptom
An allowed create returns 500 while the row is written. Boot showcase, sign up a plain member (no explicit grants), admin creates a project, then as the member:
POST /api/v1/data/showcase_task {title, project:<id>, status:'todo'}
- Expected 201 — the access matrix says
create:true and /security/explain confirms allowed=true.
- Actual 500
INTERNAL_ERROR, yet an admin read shows the task did persist with owner_id = the member. Reproduced 4×.
Server log: WARN Roll-up summary recompute failed {childObject:showcase_task, parentObject:showcase_project, field:task_count, error: Access denied: operation update on showcase_project …} ×2, then ERROR SummaryRecomputeError '…the triggering records WERE written (summary values may be stale)'.
Contrast isolating it: the identical POST succeeds 201 for showcase_contributor (holds allowEdit on showcase_project) and for admin.
Root cause
Located by the run at packages/objectql/src/engine.ts. recomputeSummaries issues the parent roll-up write under the caller's execution context (opCtx.context), so the internal recompute passes through security as the caller. showcase_project declares two Field.summary roll-ups over showcase_task, and member_default grants read but not edit on showcase_project, so the recompute is refused with PERMISSION_DENIED. withTransientRetry then retries a non-transient permission denial, and the engine throws SummaryRecomputeError, which REST maps to 500.
Confirmed still present on origin/main: recomputeSummaries (engine.ts ~L6112) calls this.update(desc.parentObject, …, { context: execCtx }) with execCtx = opCtx.context, and the update path (engine.ts ~L8478/8503) still throws SummaryRecomputeError on any failure.
Reproduction
- Boot showcase; sign up a plain
member_default persona with no explicit grants.
- As admin, create a
showcase_project.
- As the member:
POST /api/v1/data/showcase_task {title, project:<project id>, status:'todo'}.
- Observe 500
INTERNAL_ERROR; re-read as admin → the task row exists with owner_id = the member.
Suggested fix
Run the roll-up recompute in a system context — it is an engine-internal derived write, not a caller write. At minimum, stop retrying a permission denial via withTransientRetry, and stop mapping a committed write to a 500.
Source
Extracted from the QA run #7637 (framework 92f26f7, console 09987b680).
Symptom
An allowed create returns 500 while the row is written. Boot showcase, sign up a plain member (no explicit grants), admin creates a project, then as the member:
POST /api/v1/data/showcase_task {title, project:<id>, status:'todo'}create:trueand/security/explainconfirmsallowed=true.INTERNAL_ERROR, yet an admin read shows the task did persist withowner_id= the member. Reproduced 4×.Server log:
WARN Roll-up summary recompute failed {childObject:showcase_task, parentObject:showcase_project, field:task_count, error: Access denied: operation update on showcase_project …}×2, thenERROR SummaryRecomputeError '…the triggering records WERE written (summary values may be stale)'.Contrast isolating it: the identical POST succeeds 201 for
showcase_contributor(holdsallowEditonshowcase_project) and for admin.Root cause
Located by the run at
packages/objectql/src/engine.ts.recomputeSummariesissues the parent roll-up write under the caller's execution context (opCtx.context), so the internal recompute passes through security as the caller.showcase_projectdeclares twoField.summaryroll-ups overshowcase_task, andmember_defaultgrants read but not edit onshowcase_project, so the recompute is refused withPERMISSION_DENIED.withTransientRetrythen retries a non-transient permission denial, and the engine throwsSummaryRecomputeError, which REST maps to 500.Confirmed still present on
origin/main:recomputeSummaries(engine.ts ~L6112) callsthis.update(desc.parentObject, …, { context: execCtx })withexecCtx = opCtx.context, and the update path (engine.ts ~L8478/8503) still throwsSummaryRecomputeErroron any failure.Reproduction
member_defaultpersona with no explicit grants.showcase_project.POST /api/v1/data/showcase_task {title, project:<project id>, status:'todo'}.INTERNAL_ERROR; re-read as admin → the task row exists withowner_id= the member.Suggested fix
Run the roll-up recompute in a system context — it is an engine-internal derived write, not a caller write. At minimum, stop retrying a permission denial via
withTransientRetry, and stop mapping a committed write to a 500.Source
Extracted from the QA run #7637 (framework 92f26f7, console 09987b680).