componentsfix(playbook): the four things wrong with the trigger UI 1. TWO GLYPHS THAT SAID NOTHING. The meeting glyph was invisible. It asked `connectionLogo('meetings')`, which resolves to the hand-drawn icons.tsx Calendar — an `<svg fill="none">` whose path sets no fill of its own, so it inherits `none` and paints NOTHING unless the call site passes a `fill-*` class. That row passes only a size. Fixed at both ends: the glyph row now uses lucide's Calendar (which carries its own stroke), and the shared icon gets `fill="currentColor"` on its path so every other call site that passes only a size renders too. It is a presentation attribute, which a CSS class beats, so date-picker-dialog's `fill-[#9D9D9D]` is unaffected. The external-CRM glyph was sync arrows, which read as "refresh" — an action, not an event — and named neither the event nor the system. Both marks now come from the conversation timeline (Calendar, Building2), which is where a user has already learned what these events look like. The trigger badge follows suit. 2. THE MEMBER PICKER MOVES INTO THE BREADCRUMB ROW. It answers the same question the crumbs do — WHOSE playbook is this — so on its own row underneath it read as a filter over the content rather than as part of the address, and cost a row of height on every load for a control most sessions never touch. Only the picker moves: the banner and the permission notice stay in AdministeredUserBar, because they are full-width messages and squeezing either into a title row would truncate it or shove the crumbs sideways as it appears. The bar now collapses to nothing when it holds neither. 3. A REF WITH NO INSTRUCTION HAD NO WAY TO GET ONE. Every ref authored before this feature is self-closing, so it parses to the inline chip — and the chip had nowhere to put an instruction. That is not a migration to wait out; it is every ref in every playbook today. The chip now carries a second half, `+ Instructions`, which converts it into the panel with the caret already in the instruction area. Offered ONLY inside a trigger: a ref in <always-loaded> is a document the agent reads, not an agent a trigger fires, so there is no trigger for it to have an instruction for. The shared FileLinkNode does not learn what a trigger is — it asks its host "does anyone want a segment on this chip", and the playbook is the only host that answers. The conversion carries `section`/`when` across (dropping them would turn "add an instruction" into "quietly widen this ref's scope") and has two shapes: a chip alone in its paragraph replaces the paragraph, leaving no empty one behind; a chip among prose leaves the prose as the block's own note. 4. THE DISPATCH EXPLAINER MOVES INTO A `?`. It was a line under the pill. But it is REFERENCE, not status — identical for every trigger of a kind, never changing — and a sentence that never changes on a row you read daily stops being read within a week while still costing a line of height on every block in the document. Behind a `?` it is one click away on the day you need it and invisible on the days you do not. The popover answers two things in the order they are needed: what wakes it (with the four event glyphs when the trigger is `any`, since "every event" is the one label that names no thing), and what happens then — whether an orchestrator reads this block and CHOOSES, or every agent in it simply runs. 4,103 mail tests green; tsc -b clean. Co-Authored-By: Claude Opus 5 <<email>>Sep 7, 2026, 11:00 PMhooksfeat(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 AMcontext.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 AMcontract.tsfeat(agents): org admin/owner can view a teammate's agent workspace Backend (apps/server): agent.ts's 31-procedure Agent Workspace router only ever resolved an agentId inside the CALLER's own AOP set, so an org admin opening a teammate's saved agent got a generic NOT_FOUND regardless of role — the gap behind Karim's (Adapt) report that Jacob's execution logs and Deal Risk agent were unreachable even after run-sql was fixed to allow admin reads. requireAgent now accepts an optional targetUserId and routes through resolveAgentForAdmin (already built for debugTool.ts's test-run, PR #2749) instead of resolveAgentForCaller when it names someone else, gated by authorize()'s existing subagent_execution:user policy (SELF_OR_ORG_ADMIN). Every procedure's downstream reads/writes now scope to the resolved actorUserId instead of ctx.sessionUser.id — critically for runNow and createWebhookSource, which must execute/register AS the target (their own connections/credentials), not the calling admin's. duplicate stays self-only: its single-userId design has no clean cross-user answer and that's a separate product decision. Frontend (apps/mail): even with the backend fixed, nothing could reach it — CompanyExplorer already resolves a document's real owner (docOwnerUserId, off documents.getDoc) but never passed it to AgentDocumentView, which calls agent.list unscoped to map documentId -> agentId. That's the exact "document access error" Karim hit clicking into Jacob's Deal Risk agent from Brain -> Playbooks. targetUserId now threads CompanyExplorer -> AgentDocumentView -> AgentView as an explicit prop (never read ambiently via useTargetUserId(), since AgentView/AgentDocumentView also mount on surfaces with no admin picker) into every trpc.agent.* call, with contract.ts asserting each route actually accepts the field. 7 new regression tests (org-admin/plain-member/cross-org matrix, plus runSingleSubagent identity-correctness) alongside the 3 administered-user contract-coverage tests, all passing. Server + mail typecheck clean. Co-Authored-By: Claude Sonnet 5 <<email>> Claude-Session: https://claude.ai/code/session_01SfYKPYHXdMgrPLuKhXvWedSep 10, 2026, 9:04 PMindex.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.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 AM