Sub-issue of #8, which is where the permission sets these need to name get created.
Found while implementing #5. Not fixed there: naming a capability that no permission set grants would either make the action uninvokable or leave a gate that grants nothing — and src/security/index.ts is #8's file surface, not #5's.
What is ungated
Both actions added in #5 declare no requiredPermissions:
duly_catalog_apply — creates a duly_duty for anysys_user id the caller passes, in bulk. A 26-item catalog against 40 people is 1,040 rows in one call.duly_catalog_sync — rewrites cadence (frequency, due_anchor, due_offset_days, lead_days, grace_days) on every catalog-sourced duty in the org when position_code is omitted.
requiredPermissions is the real gate (ADR-0066 D4, enforced with a 403 on the platform action route and mirrored on the MCP bridge). visible/disabled are UI predicates and do not stop a request; neither action declares those either, and they would not help if it did.
Note this is not mitigated by object-level permissions: ctx.engine is the trusted facade — context-less and RLS/FLS-bypassing by design (#2849). The boundary for an action handler is the invoke-time capability gate, so with none declared there is nothing between a caller and a bulk write.
What to add
#8 already defines duly_admin as "catalog owners and rollout admins" — the right holder for both. Once that permission set exists:
// src/actions/catalog.actions.tsrequiredPermissions: ['<the duly_admin capability from #8>'],
on both CatalogApplyAction and CatalogSyncAction.
Worth considering as part of the same pass: whether duly_catalog_sync should require a narrower capability than apply. Applying a catalog to a new hire is routine onboarding; syncing org-wide rewrites authored cadence on duties people are already working to, and #5's PR notes it is deliberately reportable-after-the-fact for that reason.
Acceptance
- both actions declare
requiredPermissions naming a capability duly_admin actually grants - a test asserts the declaration (the same shape as the wiring assertions in
test/catalog-instantiate.test.ts)
Sub-issue of #8, which is where the permission sets these need to name get created.
Found while implementing #5. Not fixed there: naming a capability that no permission set grants would either make the action uninvokable or leave a gate that grants nothing — and
src/security/index.tsis #8's file surface, not #5's.What is ungated
Both actions added in #5 declare no
requiredPermissions:duly_catalog_apply— creates aduly_dutyfor anysys_userid the caller passes, in bulk. A 26-item catalog against 40 people is 1,040 rows in one call.duly_catalog_sync— rewrites cadence (frequency,due_anchor,due_offset_days,lead_days,grace_days) on every catalog-sourced duty in the org whenposition_codeis omitted.requiredPermissionsis the real gate (ADR-0066 D4, enforced with a 403 on the platform action route and mirrored on the MCP bridge).visible/disabledare UI predicates and do not stop a request; neither action declares those either, and they would not help if it did.Note this is not mitigated by object-level permissions:
ctx.engineis the trusted facade — context-less and RLS/FLS-bypassing by design (#2849). The boundary for an action handler is the invoke-time capability gate, so with none declared there is nothing between a caller and a bulk write.What to add
#8 already defines
duly_adminas "catalog owners and rollout admins" — the right holder for both. Once that permission set exists:on both
CatalogApplyActionandCatalogSyncAction.Worth considering as part of the same pass: whether
duly_catalog_syncshould require a narrower capability thanapply. Applying a catalog to a new hire is routine onboarding; syncing org-wide rewrites authored cadence on duties people are already working to, and #5's PR notes it is deliberately reportable-after-the-fact for that reason.Acceptance
requiredPermissionsnaming a capabilityduly_adminactually grantstest/catalog-instantiate.test.ts)