contract-coverage.test.tsfeat(permissions): one authority for every access decision
Consolidates every "may this caller act on this data" decision onto a single
function, and closes eleven instances of one bug class found along the way.
## The authority
services/auth/authorize.ts returns a VERDICT, never a role. There is
deliberately no isOrgAdmin on the return type, so no call site can branch on
one, and changing the permission model is a change to the DEFAULT_POLICY table
rather than a sweep through the codebase. Policy is a frozen constant keyed
(resource, scope); a missing key fails closed. resolvePolicy(orgId) is the seam
for per-org policy later, so this ships with ZERO schema changes.
Replaces canActOnTarget, resolveOrgMembership, userIsOrgAdmin, an inline
same-org check in listTasksTool, and eleven `if (!isOrgAdmin)` sites that each
re-derived the rule. One of them computed it on the wrong user.
## Holes closed
All the same shape: a caller-supplied identifier resolving to another user's
data below the gate.
applyConfigChangeTool userId input, write
readConnectionSettings userId input, read
writeDocumentTool org-linked playbook path redirect
listDocumentsTool same, ungated, plus displayDocumentTool
configureStrategistTool bare aopId, no check at all
getSentEmailsTool userId input, verbatim sent/drafted bodies
aopAgents.listForAop subagent docs to any authenticated caller
agent-action-queue 5 routes, incl. delete and replay (destructive)
custom-field-management ownerId resolved from the verdict, then ignored
overview-instance org-scope write with no tenant comparison
admin router ~45 procedures + 14 sub-routers, cross-tenant
Two were destructive, three were cross-tenant writes, one was introduced by an
earlier fix in this same change set and caught by a later sweep.
## Feature
Org admins can administer a teammate's playbook and configuration. targetUserId
is uniform across the tool layer, the tRPC layer gained the org-admin path, and
the frontend gained a scoped provider and member picker that never renders one
person's data labelled as another's.
## Enforcement
Five guards keyed on the LIVE REGISTRY, not hand-written lists, each with an
anti-vacuity floor and each proven by planting the violation:
no bare userId inputs on any registered tool
every targetUserId file reaches the authority
every mutating action is administrable or documented self-only
role reads confined to the authority (depcruise rule + source scan)
redirect sites consult the authority about the RESOLVED target
## Notes
- agentExecutionEnabled is Cedar-staff-only, enforced in the single writer.
Stronger than SELF, so deliberately not a policy row.
- playbook:org and document:org are ANY_MEMBER, matching today's behaviour.
57 of 73 orgs have no admin; tightening is one cell once they do.
- Fixes two org-role gates that read undefined on every request and so refused
everyone, including statistics.getOrgOverview.
- config-write split: apply-change moved to connection-write. 17,615 -> 10,142
bytes against the connector's silent-drop ceiling.
Verified: server 799 files / 9,310 tests; mail 316 suites / 2,959 tests; types
and deps:check clean. End-to-end against the production Cedar org with a
temporary non-staff account, 42 assertions across member/admin/viewer, since
staff accounts cannot exercise the org-grant branch. Account deleted and org
state verified identical to its pre-test backup.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 8:01 AMpermission-error.test.tsfeat(permissions): one authority for every access decision
Consolidates every "may this caller act on this data" decision onto a single
function, and closes eleven instances of one bug class found along the way.
## The authority
services/auth/authorize.ts returns a VERDICT, never a role. There is
deliberately no isOrgAdmin on the return type, so no call site can branch on
one, and changing the permission model is a change to the DEFAULT_POLICY table
rather than a sweep through the codebase. Policy is a frozen constant keyed
(resource, scope); a missing key fails closed. resolvePolicy(orgId) is the seam
for per-org policy later, so this ships with ZERO schema changes.
Replaces canActOnTarget, resolveOrgMembership, userIsOrgAdmin, an inline
same-org check in listTasksTool, and eleven `if (!isOrgAdmin)` sites that each
re-derived the rule. One of them computed it on the wrong user.
## Holes closed
All the same shape: a caller-supplied identifier resolving to another user's
data below the gate.
applyConfigChangeTool userId input, write
readConnectionSettings userId input, read
writeDocumentTool org-linked playbook path redirect
listDocumentsTool same, ungated, plus displayDocumentTool
configureStrategistTool bare aopId, no check at all
getSentEmailsTool userId input, verbatim sent/drafted bodies
aopAgents.listForAop subagent docs to any authenticated caller
agent-action-queue 5 routes, incl. delete and replay (destructive)
custom-field-management ownerId resolved from the verdict, then ignored
overview-instance org-scope write with no tenant comparison
admin router ~45 procedures + 14 sub-routers, cross-tenant
Two were destructive, three were cross-tenant writes, one was introduced by an
earlier fix in this same change set and caught by a later sweep.
## Feature
Org admins can administer a teammate's playbook and configuration. targetUserId
is uniform across the tool layer, the tRPC layer gained the org-admin path, and
the frontend gained a scoped provider and member picker that never renders one
person's data labelled as another's.
## Enforcement
Five guards keyed on the LIVE REGISTRY, not hand-written lists, each with an
anti-vacuity floor and each proven by planting the violation:
no bare userId inputs on any registered tool
every targetUserId file reaches the authority
every mutating action is administrable or documented self-only
role reads confined to the authority (depcruise rule + source scan)
redirect sites consult the authority about the RESOLVED target
## Notes
- agentExecutionEnabled is Cedar-staff-only, enforced in the single writer.
Stronger than SELF, so deliberately not a policy row.
- playbook:org and document:org are ANY_MEMBER, matching today's behaviour.
57 of 73 orgs have no admin; tightening is one cell once they do.
- Fixes two org-role gates that read undefined on every request and so refused
everyone, including statistics.getOrgOverview.
- config-write split: apply-change moved to connection-write. 17,615 -> 10,142
bytes against the connector's silent-drop ceiling.
Verified: server 799 files / 9,310 tests; mail 316 suites / 2,959 tests; types
and deps:check clean. End-to-end against the production Cedar org with a
temporary non-staff account, 42 assertions across member/admin/viewer, since
staff accounts cannot exercise the org-grant branch. Account deleted and org
state verified identical to its pre-test backup.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 8:01 AMscope-not-honoured-notice.test.tsxfeat(permissions): one authority for every access decision
Consolidates every "may this caller act on this data" decision onto a single
function, and closes eleven instances of one bug class found along the way.
## The authority
services/auth/authorize.ts returns a VERDICT, never a role. There is
deliberately no isOrgAdmin on the return type, so no call site can branch on
one, and changing the permission model is a change to the DEFAULT_POLICY table
rather than a sweep through the codebase. Policy is a frozen constant keyed
(resource, scope); a missing key fails closed. resolvePolicy(orgId) is the seam
for per-org policy later, so this ships with ZERO schema changes.
Replaces canActOnTarget, resolveOrgMembership, userIsOrgAdmin, an inline
same-org check in listTasksTool, and eleven `if (!isOrgAdmin)` sites that each
re-derived the rule. One of them computed it on the wrong user.
## Holes closed
All the same shape: a caller-supplied identifier resolving to another user's
data below the gate.
applyConfigChangeTool userId input, write
readConnectionSettings userId input, read
writeDocumentTool org-linked playbook path redirect
listDocumentsTool same, ungated, plus displayDocumentTool
configureStrategistTool bare aopId, no check at all
getSentEmailsTool userId input, verbatim sent/drafted bodies
aopAgents.listForAop subagent docs to any authenticated caller
agent-action-queue 5 routes, incl. delete and replay (destructive)
custom-field-management ownerId resolved from the verdict, then ignored
overview-instance org-scope write with no tenant comparison
admin router ~45 procedures + 14 sub-routers, cross-tenant
Two were destructive, three were cross-tenant writes, one was introduced by an
earlier fix in this same change set and caught by a later sweep.
## Feature
Org admins can administer a teammate's playbook and configuration. targetUserId
is uniform across the tool layer, the tRPC layer gained the org-admin path, and
the frontend gained a scoped provider and member picker that never renders one
person's data labelled as another's.
## Enforcement
Five guards keyed on the LIVE REGISTRY, not hand-written lists, each with an
anti-vacuity floor and each proven by planting the violation:
no bare userId inputs on any registered tool
every targetUserId file reaches the authority
every mutating action is administrable or documented self-only
role reads confined to the authority (depcruise rule + source scan)
redirect sites consult the authority about the RESOLVED target
## Notes
- agentExecutionEnabled is Cedar-staff-only, enforced in the single writer.
Stronger than SELF, so deliberately not a policy row.
- playbook:org and document:org are ANY_MEMBER, matching today's behaviour.
57 of 73 orgs have no admin; tightening is one cell once they do.
- Fixes two org-role gates that read undefined on every request and so refused
everyone, including statistics.getOrgOverview.
- config-write split: apply-change moved to connection-write. 17,615 -> 10,142
bytes against the connector's silent-drop ceiling.
Verified: server 799 files / 9,310 tests; mail 316 suites / 2,959 tests; types
and deps:check clean. End-to-end against the production Cedar org with a
temporary non-staff account, 42 assertions across member/admin/viewer, since
staff accounts cannot exercise the org-grant branch. Account deleted and org
state verified identical to its pre-test backup.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 8:01 AM