CedarCopilot wants to merge 9 commits into staging from feat/permissions-authorize
Live on prod since Wed, 1 of 8 behaviors working
Post-deploy production telemetry shows healthy Background jobs activity: scheduled_task_queue_processing ran 16 times across 12 users <id> 59.2s, p99 62.1s); processScheduledEmails produced 32 spans, all STATUS_CODE_OK (p95 171ms, p99 236ms), with 32 matching cron success.
Strict CloudWatch fallback saw 1,988 success-shaped log lines matching pipeline, analytics, quota, aop, but no tied operation was present, so Libra is not calling this working.
Strict CloudWatch fallback found 10 recent prod failure log lines for [processIncomingSlackChannelData] Error processing channel (No valid connection tokens found for user <id>), but the same failure had 5 log lines in the comparable pre-deploy baseline. Libra is not blaming.
Strict CloudWatch fallback found 254 prod failure log lines for mirror.upsertEmailThread failed (Failed query: insert into \), but this domain-wide failure family is not tied to this intent.
Strict CloudWatch fallback found 143 recent prod failure log lines for [UpdateExternalCrmWorkflow] Driver update failed (Step updateOpportunityField failed: HTTP 400: [{\), but the same failure had 165 log lines in the comparable pre-deploy baseline. Libra is not blaming this.
Verification reached its 120s wall-clock budget before a tied verdict. Libra is keeping this intent verifying until the next check.
internal_only
Libra has verdicts on 1 of 8 tracked behaviors on prod; 7 are still being checked. Libra checks hourly for 3 days after each deploy.
Every "may this caller act on this data" decision in the codebase now routes through one function, services/auth/authorize.ts. It 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.
Ships with zero schema changes. resolvePolicy(orgId) is a private seam that returns a frozen constant today; when the model is decided it becomes a column read and no call site changes.
It 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.
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 (~20 routes), and the frontend gained a scoped provider and member picker built so that no surface ever renders one person's data labelled as another's.
All the same shape: a caller-supplied identifier resolving to another user's data below the gate.
| Site | Identifier | Severity |
|---|---|---|
applyConfigChangeTool | userId input | cross-tenant write |
readConnectionSettingsTool | userId input | cross-tenant read |
writeDocumentTool | org-linked playbook path | cross-tenant write |
listDocumentsTool / displayDocumentTool | same path, ungated | metadata leak |
configureStrategistTool | bare aopId, no check at all | cross-tenant write |
getSentEmailsTool | userId input | verbatim sent/drafted bodies |
aopAgents.listForAop | aopId | subagent docs to any caller |
agent-action-queue (5 routes) | userId / runId | destructive: delete, replay |
custom-field-management | ownerId resolved then ignored | cross-user + cross-org write |
overview-instance | org scope, no tenant comparison | cross-tenant write |
admin router + 14 sub-routers | ~45 procedures | reads, deletes, fleet enumeration |
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.
Every one is pre-existing. Nothing here got worse; it got visible.
Five guards keyed on the live registry, not hand-written lists, each with an anti-vacuity floor, each proven by planting the violation and watching it fail:
userId inputs on any registered tooltargetUserId reaches the authorityThe last one matters because a call site that asks the authority about the caller passes any "is authorize() called here" review and still leaks.
agentExecutionEnabled is Cedar-staff-only, enforced in the single writer so no path can miss it. Stronger than SELF (the account owner cannot set it either), so deliberately not a policy row: every capability in that table is something an org can be granted, and this is grantable to nobody.playbook:org and document:org are ANY_MEMBER, matching today's behaviour. 57 of 73 orgs have no admin at all, and 1,418 documents live under organisation/; ORG_ADMIN would have locked members out of their own knowledge base. Tightening is one cell once admins exist.ctx.sessionUser.organizationRole is not in better-auth's additionalFields, so it read undefined on every request and statistics.getOrgOverview refused everyone. Now works. This is a real widening from nobody to admins.Libra has not measured any production surfaces for this change yet.