What was measured
The per-declared-position probe personas added for #7978 run one persona per position the app declares. On examples/app-crm, all three declared positions report probe-blocked on all 6 objects — 18 of 18 probes:
coverage: declared=3 [sales_rep,sales_manager,finance_approver] ran=3 notRun=[]
sales_rep proven=0 unproven=6 (probeBlocked=6)
sales_manager proven=0 unproven=6 (probeBlocked=6)
finance_approver proven=0 unproven=6 (probeBlocked=6)
probe-blocked means the OBJECT gate answered 403 on a plain LIST: the persona holds no object grant at all, so record scope was never consulted.
Why
The app declares both halves and never joins them:
examples/app-crm/src/security/sales-positions.ts declares sales_rep / sales_manager / finance_approver, and separately declares the crm_sales_user permission set granting read/create/edit on the five CRM objects.- Nothing binds them. There is no
sys_position_permission_set seeding (the showcase does this imperatively in src/security/bind-position-sets.ts, on kernel:bootstrapped), and crm_sales_user is not marked isDefault, so it is not auto-bound to the everyone anchor either.
So a user an admin assigns sales_rep to resolves only the platform baseline and gets 403 on every CRM object. The three sharedWith: { type: 'position', value: 'sales_manager' } sharing rules can widen nothing on top of an object gate that is closed.
Grading
Filed as an observation, not a defect report: nothing in CI or in a shipped user flow hits this today — the CRM example seeds no persona users, and in a real deployment binding a set to a position is an admin action in Setup. What it does mean is that the CRM example demonstrates positions and permission sets as two declarations that never meet, while the showcase demonstrates the binding. Whether that is worth aligning (one bind-position-sets-style hook, or marking the set isDefault) is a product call for triage.
Found while implementing #7978; not in that PR's scope.
What was measured
The per-declared-position probe personas added for #7978 run one persona per position the app declares. On
examples/app-crm, all three declared positions reportprobe-blockedon all 6 objects — 18 of 18 probes:probe-blockedmeans the OBJECT gate answered 403 on a plain LIST: the persona holds no object grant at all, so record scope was never consulted.Why
The app declares both halves and never joins them:
examples/app-crm/src/security/sales-positions.tsdeclaressales_rep/sales_manager/finance_approver, and separately declares thecrm_sales_userpermission set granting read/create/edit on the five CRM objects.sys_position_permission_setseeding (the showcase does this imperatively insrc/security/bind-position-sets.ts, onkernel:bootstrapped), andcrm_sales_useris not markedisDefault, so it is not auto-bound to theeveryoneanchor either.So a user an admin assigns
sales_repto resolves only the platform baseline and gets 403 on every CRM object. The threesharedWith: { type: 'position', value: 'sales_manager' }sharing rules can widen nothing on top of an object gate that is closed.Grading
Filed as an observation, not a defect report: nothing in CI or in a shipped user flow hits this today — the CRM example seeds no persona users, and in a real deployment binding a set to a position is an admin action in Setup. What it does mean is that the CRM example demonstrates positions and permission sets as two declarations that never meet, while the showcase demonstrates the binding. Whether that is worth aligning (one
bind-position-sets-style hook, or marking the setisDefault) is a product call for triage.Found while implementing #7978; not in that PR's scope.