feat(org): treat platform admins as members in internal organizations#3378
Conversation
Adds `Organization.isInternal` so platform-operated organizations can run compliance with their platform-admin staff as real participants — assignable, counted toward compliance progress, and included in notifications and device rollups. Behavior is unchanged for every other organization (flag defaults to false; admin-only toggle on the org admin screen). Centralizes the rule behind a single `isOrgParticipant` predicate (`@trycompai/auth/participation`, mirrored for the Trigger.dev bundle), replacing the scattered inline `user.role === 'admin'` participation checks across assignment/approval, compliance counting, notifications, reminders, and device queries. Platform-admin access guards are intentionally untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWG1YQ8KsjefFAvRV7pdtb
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
All reported issues were addressed across 41 files
Confidence score: 5/5
- Safe to merge after the addressed issues were fixed.
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
…idation, keep dep boundary - run-browser-automation: use the shared `orgParticipantMemberWhere` helper instead of an inline copy, so it matches the other task notifiers (uses the role constant + owner exception) and still includes everyone in internal orgs. - update-admin-organization DTO: reject an explicit `null` isInternal at validation (400) instead of letting it reach the non-null column (500). - org-participation (app): stop re-exporting `isOrgParticipant` from the module that imports `@db/server`; consumers import it from the dependency-free `org-participation-rule` so server-only code can't leak into client/Trigger bundles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWG1YQ8KsjefFAvRV7pdtb
|
@cubic-dev-ai review it |
@tofikwest I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 41 files
Confidence score: 5/5
- Safe to merge after the addressed issues were fixed.
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
…rule, guard boundaries - generate-vendor-mitigation: fail closed — only assign the author when the member actually exists in this org (add `author &&` guard) so a missed lookup can never write an unknown/cross-org member id. - SelectAssignee: use the shared `isOrgParticipant` predicate instead of an inline copy, so the picker can't drift from the backend rule. - lib/org-participation.ts: add `import 'server-only'` (matches compliance.ts / permissions.server.ts) so this DB-backed helper can't be pulled into a client bundle. - Add a drift guard test asserting the app-local participation rule stays identical to `@trycompai/auth/participation`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWG1YQ8KsjefFAvRV7pdtb
|
Reviewed the latest cubic pass (5 issues). Verdict + resolution below — fixes in Fixed (valid):
Not a real / new issue — marking as won't-fix:
All source typechecks clean (only the pre-existing |
|
@cubic-dev-ai review it |
@tofikwest I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 41 files
Confidence score: 5/5
- Safe to merge after the addressed issues were fixed.
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
…r, skip needless query - orgParticipantMemberWhere: make it a faithful Prisma translation of isOrgParticipant — for non-internal orgs exclude only platform admins (include null roles; drop the owner carve-out, which the isInternal flag now supersedes). Removes the split source of truth between the predicate and the query fragment. - generate-vendor-mitigation: add `deactivated: false` to the author lookup so a deactivated member can't be auto-assigned (matches the other trigger tasks). - filterComplianceMembers (app + api): skip the `getOrgIsInternal` query when the member list has no platform admins — the flag can't change the result there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWG1YQ8KsjefFAvRV7pdtb
|
Third cubic pass (3 issues) — all addressed in
Source typechecks clean (only the pre-existing |
|
@cubic-dev-ai review it |
@tofikwest I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 41 files
Confidence score: 5/5
- Safe to merge after the addressed issues were fixed.
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
…ternal toggle
- org-participation: return the participant where-fragment wrapped in `AND`
(via new `orgParticipantMemberWhereForFlag`) so spreading it no longer
overwrites a caller's existing `user` filter. The two mention notifiers
constrain `user: { id: { in } }`, which the bare `user` key clobbered, so
notifications reached every non-admin member instead of the mentioned ones.
Adds a spread-safety unit test.
- OrganizationDetail: refresh the server layout (router.refresh) after toggling
isInternal for the current org so OrgInternalProvider isn't stale, and require
a confirmation dialog before changing this org-wide setting.
- run-browser-automation: select `isInternal` in the existing org query and use
the pure where-builder, dropping the redundant getOrgIsInternal query.
- Normalize participation onto isOrgParticipant everywhere: drop the per-member
owner carve-out from weekly-task-reminder, task-schedule, policy-schedule and
the task-item assignment notifier (matches the API where-fragment and the
predicate); remove now-dead role selections and stale comments.
- tests: add a PointerEvent shim to the vitest setup (jsdom lacks it, base-ui
Switch needs it), fix admin-org test mocks missing the new isInternal field,
and cover the internal-toggle confirmation flow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzEVwhhoFghng6SDMGecMq
|
Addressed the latest cubic pass (commit 26cecbe). Verdict + resolution below. Fixed (valid):
Extra cleanup: fixed the admin-org test mocks that were missing the new required API participation/notifier/SOA/task-management suites pass (98); app participation + org-detail suites pass. Remaining typecheck failures are all pre-existing and in files this PR doesn't touch. |
…rticipation # Conflicts: # apps/app/src/app/api/people/agent-devices/route.ts
|
@cubic-dev-ai review it |
@tofikwest I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 44 files
Confidence score: 5/5
- Safe to merge after the addressed issues were fixed.
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
The assignee guards only threw when a member was found in the org and failed the
participant check. When the org-scoped `member.findFirst` returned null — the id
belongs to another org or doesn't exist — the guard was skipped and the id was
persisted, since assignee columns don't enforce same-org membership. This gap is
pre-existing (the prior `member?.user.role === 'admin'` check had the same null
hole); hardened here while the participation refactor already touches these
guards. Every assignee guard now rejects a non-member with a clear 400.
Covers tasks (bulk + single update), task items (create + update), policies
(create), vendors, and risks. Approver guards already rejected null members.
Also consolidate task-notifier's automation-failure paths, which fetched the org
twice (once for isInternal via orgParticipantMemberWhere, once for name), into a
single `{ name, isInternal }` lookup + orgParticipantMemberWhereForFlag.
Adds a regression test for the cross-org assignee rejection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzEVwhhoFghng6SDMGecMq
|
Latest cubic pass (run Fixed (real issues):
Intentional (confirming, not a bug):
Already addressed:
Resolving these threads accordingly. |
|
🎉 This PR is included in version 3.101.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
What
Adds an
Organization.isInternalflag so platform-operated organizations (e.g. our own) can run compliance with their platform-admin staff as real members — assignable, counted toward compliance progress, and included in notifications, reminders, and device rollups.For every other organization the behavior is unchanged, with one deliberate exception: a global platform admin who also holds an
ownermember role in a non-internal org is now excluded from notifications/mentions/reminders, whereas some of those paths previously kept them. This normalizes notifications onto the same rule the assignment/approval guards already used (which never had an owner carve-out), so the participation rule is consistent everywhere. The flag defaults tofalse, and it's an admin-only toggle on the org admin screen.Why
Platform admins (global staff accounts) are intentionally excluded from an organization's day-to-day membership everywhere in the product. That's correct for the orgs they only enter to support. It's wrong for a platform-operated org, where those same accounts are the real members and need to run compliance like anyone else.
How
isOrgParticipant(userRole, { orgIsInternal }), in@trycompai/auth(new@trycompai/auth/participationsubpath so it stays dependency-light; mirrored in a dependency-free app helper for the Trigger.dev bundle).user.role === 'admin'participation checks across assignment/approval, compliance counting, notifications/reminders, and device queries with that single rule.isInternaladded to the admin org update endpoint + a "Platform settings → Internal organization" switch on the org detail screen.Data
20260708120000_add_organization_is_internaladdsisInternal BOOLEAN NOT NULL DEFAULT false.Tests
Rollout
Flag is off everywhere on merge. Enable per-org from the admin screen (or
PATCH /v1/admin/organizations/:id { isInternal: true }) only for platform-operated orgs.🤖 Generated with Claude Code
https://claude.ai/code/session_01UWG1YQ8KsjefFAvRV7pdtb
Summary by cubic
Treat platform admins as real members in internal (platform‑operated) orgs via
Organization.isInternal, unifying participation behindisOrgParticipantacross API, app, and Trigger.dev. Also hardens assignment to reject non‑member IDs and fixes mention notifications to only reach the mentioned users.New Features
isInternaladded to the admin org update DTO and endpoint (explicitnullnow 400s).isOrgParticipantin@trycompai/auth/participation(mirrored in the app for Trigger.dev) with helpersisMemberOrgParticipant,orgParticipantMemberWhere, andorgParticipantMemberWhereForFlagthat return a spread‑safe AND fragment. Replaces inline checks across policies/tasks/risks/vendors/SOA, notifications/mentions, device APIs, scheduled tasks, browser automation, and the assignee picker.Migration
Organization.isInternal BOOLEAN NOT NULL DEFAULT false(migration20260708120000_add_organization_is_internal).PATCH /v1/admin/organizations/:id { isInternal: true }.Written for commit dd071b6. Summary will update on new commits.