Skip to content

feat: org agent management#259

Merged
RambokDev merged 6 commits intomainfrom
feat/org-agent
Apr 10, 2026
Merged

feat: org agent management#259
RambokDev merged 6 commits intomainfrom
feat/org-agent

Conversation

@RambokDev
Copy link
Copy Markdown
Collaborator

@RambokDev RambokDev commented Apr 8, 2026

Summary by CodeRabbit

  • New Features

    • Organization-level agent management: new Agents tab, settings pages, and dialogs to create, view and assign agents to organizations
    • Organization-scoped available databases view and improved agent organization assignment UI
  • Bug Fixes

    • Agent and database lists now exclude archived/unscoped agents and enforce organization scoping
    • Breadcrumbs treat "Settings" as navigable
  • Refactor

    • More consistent timestamp handling on updates and safer agent deletion behavior for organization-scoped removals

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

Caution

Review failed

Pull request was closed or merged during review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds organization-scoped agent management: DB migrations and schema for organization-agent joins, new services and pages for org-scoped agents, UI/forms/actions to manage agent↔organization links, ACL permission for agents, and consistent timestamp updates across several DB updates.

Changes

Cohort / File(s) Summary
Database schema & migrations
src/db/schema/08_agent.ts, src/db/schema/07_database.ts, src/db/schema/02_user.ts, src/db/migrations/0049_chief_terrax.sql, src/db/migrations/0050_dark_saracen.sql, src/db/migrations/0051_young_senator_kelly.sql, src/db/migrations/0052_cute_punisher.sql, src/db/migrations/meta/*_snapshot.json, src/db/migrations/meta/_journal.json
Add organization_agents join table and agents.organization_id column; add two_factor.verified; update schema types (AgentWith, DatabaseWith) and include multiple migration snapshots.
Org-scoped pages & routing
app/(customer)/dashboard/(organization)/settings/agents/[agentId]/page.tsx, app/(customer)/dashboard/(organization)/settings/agents/page.tsx, app/(customer)/dashboard/(organization)/settings/page.tsx, app/(customer)/dashboard/(organization)/projects/*, app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx
Add organization agent detail page with ACL/ownership checks and redirect entry page; wire organization agents into settings tabs; replace inline DB query with service call for available databases.
Admin agent pages
app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx, app/(customer)/dashboard/(admin)/agents/page.tsx
Eager-load agent organizations, compute organization-derived ownership, pass organization IDs to delete component, and adjust agent list filter to exclude archived/null-organization entries.
Services
src/db/services/agent.ts, src/db/services/database.ts
Add getOrganizationAgents() and getOrganizationAvailableDatabases() to fetch agents/databases scoped/filtered by organization membership and archived flags.
Agent create/update/delete logic
src/features/agents/agents.action.ts, src/components/wrappers/dashboard/agent/button-delete-agent/delete-agent.action.ts, src/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent.tsx
Create action accepts optional organizationId and inserts join row; delete action input becomes object with org scope, unlinks orgs, clears/updates related databases and policies, archives agent with timestamped updates; UI props updated.
Agent UI, dialogs & forms
src/features/agents/components/agent.dialog.tsx, src/features/agents/components/agent.form.tsx, src/features/agents/components/agent-organizations.action.ts, src/features/agents/components/agent-organizations.form.tsx, src/features/agents/components/agent-organizations.schema.ts, src/components/wrappers/dashboard/organization/tabs/.../organization-agents-tab.tsx, src/components/wrappers/dashboard/agent/agent-card/agent-card.tsx
Extend AgentDialog for adminView/organizations tab; add AgentOrganisationForm and update action; add OrganizationAgentsTab UI; add organizationView routing prop for agent cards.
ACL & permissions
src/lib/acl/organization-acl.ts
Add canManageAgents permission and include it in computed organization permissions.
Timestamp/update consistency
app/api/agent/.../backup/upload/status/route.ts, app/api/agent/.../restore/route.ts, app/api/agent/.../status/helpers.ts, multiple admin settings action files, src/utils/init.ts
Wrap update payloads with withUpdatedAt(...) to ensure updated timestamps are set on various DB updates.
UI & small schema tweaks
src/components/wrappers/common/bread-crumbs/bread-crumbs.tsx, src/components/wrappers/common/empty-state-placeholder.tsx, src/components/wrappers/dashboard/admin/channels/.../channels-organization.schema.ts, src/components/wrappers/dashboard/database/import/upload-backup-zone.tsx, src/components/wrappers/dashboard/organization/delete-organization-button.tsx
Remove "settings" from forbidden breadcrumb links; add state prop to empty placeholder; tighten UUID validation for organization lists; remove debug log; add disabled prop to delete-org button.
Deps / tooling / misc
package.json, Makefile, small comment/cleanup files
Dependency upgrades (Better Auth, Drizzle, Zod, React Query, etc.); Makefile mount path change; minor imports/whitespace/comment fixes.

Sequence Diagram

sequenceDiagram
    participant User as User (Org Member)
    participant Browser as Browser
    participant OrgPage as Org Settings Page (Server)
    participant ACL as Org ACL Service
    participant DB as Database
    participant AgentSvc as Agent Service

    User->>Browser: request org agent settings page
    Browser->>OrgPage: SSR request with params
    OrgPage->>ACL: load org, user, activeMember
    ACL-->>OrgPage: permissions (canManageAgents)
    alt not authorized
        OrgPage-->>Browser: notFound()
    else authorized
        OrgPage->>DB: fetch agent (with organizations, databases)
        DB-->>OrgPage: agent + relations
        OrgPage->>OrgPage: verify org ownership/scoping
        OrgPage-->>Browser: render page (adminView if owned)
        User->>Browser: submit org-assignment form
        Browser->>AgentSvc: updateAgentOrganizationsAction(payload)
        AgentSvc->>DB: fetch agent + current orgs + projects
        AgentSvc->>DB: insert/delete organization_agent rows
        alt orgs removed
            AgentSvc->>DB: clear database.projectId & backupPolicy (withUpdatedAt)
            AgentSvc->>DB: delete retention/alert/storage policies
        end
        DB-->>AgentSvc: success
        AgentSvc-->>Browser: success response
        Browser->>Browser: show toast & refresh
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

Poem

🐰
I hopped through schemas, joins, and tabs today,
Linking agents to orgs in a neat display.
With timestamps ticked and ACLs aligned,
Cards and dialogs now all combined.
Hooray — I nibble bugs and hop away!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat: org agent management' accurately describes the main objective—introducing organization-level agent management capabilities—evidenced by new agent settings pages, organization-scoped agent queries, agent-organization relationships, and UI components throughout the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/org-agent

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 21

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/features/agents/components/agent.form.tsx (1)

67-70: ⚠️ Potential issue | 🟠 Major

Make the fallback redirect org-aware.

This form can now create organization-owned agents, but the default success path still pushes to the admin detail route. That route now 404s for agents with organizationId, so org-scoped creates will land on a dead page unless every caller overrides onSuccess.

Proposed fix
-            } else {
-                router.push(`/dashboard/agents/${data.id}`);
+            } else if (props.organization) {
+                router.push("/dashboard/settings?tab=agents");
+            } else {
+                router.push(`/dashboard/agents/${data.id}`);
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/agents/components/agent.form.tsx` around lines 67 - 70, The
fallback redirect in the success handler currently always does
router.push(`/dashboard/agents/${data.id}`) which 404s for org-owned agents;
update the fallback in the component (the props.onSuccess branch in
agent.form.tsx) to check data.organizationId and, if present, push to the
org-scoped agent route (include data.organizationId and data.id in the URL),
otherwise keep the existing admin route; modify the code around props.onSuccess
and router.push to choose the route based on data.organizationId.
src/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent.tsx (1)

22-27: ⚠️ Potential issue | 🟠 Major

Choose the post-delete redirect from the current scope.

This component is now used from organization settings as well, but success still hardcodes "/dashboard/agents". After an org-scoped delete, that sends users to the global agents screen instead of back to the organization UI, and may even route them to a page they cannot access. Base the redirect on props.organizationId.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent.tsx`
around lines 22 - 27, The onSuccess handler currently always calls
router.push("/dashboard/agents") after delete; change it to choose the
post-delete redirect based on props.organizationId so org-scoped deletes return
to the organization UI. In the onSuccess of the mutationFn (where
deleteAgentAction is called and props.agentId/props.organizationId are
available), replace the hardcoded router.push("/dashboard/agents") with a
conditional that pushes the organization-specific route when
props.organizationId is set (e.g. the org agents list) and falls back to the
global "/dashboard/agents" otherwise; update any tests/consumers that assume the
previous hardcoded redirect if needed.
src/components/wrappers/dashboard/organization/tabs/organization-tabs.tsx (1)

84-111: 🛠️ Refactor suggestion | 🟠 Major

Guard the new tab with canManageAgents.

The ACL layer already exposes a dedicated agent-management permission, but this tab is rendered under the existing notification/storage gate instead of its own flag. That works only while those permissions happen to match; if they diverge, agent management will be shown or hidden behind the wrong ACL.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/wrappers/dashboard/organization/tabs/organization-tabs.tsx`
around lines 84 - 111, The Agents tab is currently shown/hidden under the wrong
permission; update organization-tabs.tsx to guard the Agents tab with the
dedicated canManageAgents flag: wrap (or conditionally render) the TabsTrigger
with value="agents" and the corresponding TabsContent value="agents" (which
renders OrganizationAgentsTab with prop agents) behind canManageAgents instead
of the notification/storage permission. Ensure both the tab trigger and its
content check canManageAgents so visibility is consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/`(customer)/dashboard/(admin)/agents/[agentId]/page.tsx:
- Around line 33-38: The organizations DB query (db.query.organization.findMany
that assigns organizations) runs before the notFound() guards; move the
organizations lookup so it executes after all validation/guard checks (the
notFound() calls that validate agentId and ownership) to avoid doing heavy work
on 404 paths, and apply the same relocation for the second organizations lookup
block (the other db.query.organization.findMany instance) so both are executed
only after the guards pass.

In `@app/`(customer)/dashboard/(admin)/agents/page.tsx:
- Line 19: The current filter uses the nullable field
drizzleDb.schemas.agent.organizationId (where:
and(not(eq(drizzleDb.schemas.agent.isArchived, true)),
isNull(drizzleDb.schemas.agent.organizationId))) which is incorrect for the new
many-to-many org-agent model; replace this nullable check with a membership
check against the join table (organization_agents) — e.g., change the where
clause to filter agents that do not have a related row in
drizzleDb.schemas.organization_agents (use a NOT EXISTS subquery or a left join
to organization_agents and assert the join key is null) so the admin agent list
reflects join-table association instead of agents.organizationId.

In `@app/`(customer)/dashboard/(organization)/projects/[projectId]/page.tsx:
- Around line 60-72: Remove the retired inline query block by deleting the
commented-out availableDatabases declaration and its db.query.database.findMany
call (the commented lines referencing availableDatabases and
db.query.database.findMany), since the new service call is the single source of
truth; ensure no leftover references to availableDatabases remain in this file
(page.tsx) so only the service-based data flow is used.

In `@app/`(customer)/dashboard/(organization)/settings/agents/[agentId]/page.tsx:
- Line 63: The isOwned check currently uses the truthiness of
agent.organizationId which only indicates presence, not ownership; update the
variable (isOwned) to compare agent.organizationId === organization.id so it
only returns true when the current organization actually owns the agent
(referencing agent and organization.id), or if the original intent was merely to
detect presence, rename isOwned to hasOwner to clarify intent.
- Line 82: The ButtonDeleteAgent is being passed organizationId={organization.id
?? null} but ButtonDeleteAgentProps expects string | undefined; replace the null
with undefined (or just pass organization.id directly) so organizationId is a
string or undefined — update the JSX at ButtonDeleteAgent to use organization.id
(or organization.id ?? undefined) and keep agentId as-is.

In
`@src/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.action.ts`:
- Around line 44-45: Remove the "// `@ts-ignore`" on the .set({ ...data }) call
and fix the update payload by mapping StorageSwitchSchema fields to actual
settings table columns instead of a nonexistent "storage" column; follow the
pattern used in updateS3SettingsAction by wrapping the update with
withUpdatedAt() and pass an explicit object of valid column names (e.g.,
enabled/region/bucket or whatever fields your settings table expects) to
.set(...) so the types align and no ts-ignore is needed.

In
`@src/components/wrappers/dashboard/agent/button-delete-agent/delete-agent.action.ts`:
- Around line 66-69: The success metadata is using the wrong key; update
actionSuccess.messageParams so it uses agentId instead of projectId. Locate the
actionSuccess object in delete-agent.action.ts (the actionSuccess block) and
change messageParams from {projectId: agentId} to {agentId: agentId} (or
shorthand {agentId}) so templates/log consumers receive the expected agentId
key.
- Around line 28-48: The current flow deletes a single organization_agents row
then always archives the agent (in the block that uses organizationId and in the
admin path), which lets one org disable an agent for all orgs and leaves stale
associations; change the logic in delete-agent.action.ts to branch: if
organizationId is provided, run a transaction that deletes only the matching
organizationAgent row (drizzleDb.schemas.organizationAgent) and does not archive
the agent, and if no organizationId is provided (global delete/admin path) run a
transaction that archives the agent row (drizzleDb.schemas.agent: set
isArchived, slug, deletedAt) and deletes all related organizationAgent rows
atomically; ensure you use the existing db transaction API and the same schema
identifiers (organizationAgent, agent) and where predicates (organizationId,
agentId) so associations are cleaned up in one atomic operation for each path.

In
`@src/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-agents-tab.tsx`:
- Around line 32-37: Remove the commented-out ChannelAddEditModal block: delete
the commented JSX lines referencing ChannelAddEditModal and its props (kind,
organization, open/isAddModalOpen, onOpenChangeAction/setIsAddModalOpen) from
organization-agents-tab.tsx; if the modal functionality is intended for future
work, create a tracking issue instead of leaving the dead code commented in
place.
- Line 19: The state pair isAddModalOpen / setIsAddModalOpen in the
OrganizationAgentsTab component is unused; either remove the declaration to
eliminate dead code or wire it up to the intended add-agent modal: implement
openAddModal/closeAddModal handlers that call setIsAddModalOpen(true/false),
pass isAddModalOpen to the modal’s open/visible prop and set onClose to call
setIsAddModalOpen(false), and ensure any "Add" button calls openAddModal; if
removing, also delete any unused imports related to the modal.

In `@src/db/migrations/0049_chief_terrax.sql`:
- Around line 1-5: The migration for organization_agents is missing the
created_at column and the composite primary key specified in the schema; update
the CREATE TABLE for organization_agents to include a created_at TIMESTAMP WITH
TIME ZONE NOT NULL DEFAULT now(), make ("organization_id","agent_id") the
PRIMARY KEY instead of a UNIQUE constraint, and add explicit foreign key
constraints for organization_id and agent_id referencing the respective parent
tables (preserve column names organization_id and agent_id). Ensure the column
type and default match the schema's created_at definition so relation queries
expecting created_at and the composite PK succeed.

In `@src/db/schema/08_agent.ts`:
- Around line 58-64: The AgentWith type is inconsistent: databases is optional
but organizations is required, which causes type errors when agents are loaded
without eager-loading organizations; update the AgentWith definition (the
AgentWith type in src/db/schema/08_agent.ts) to make organizations optional and
allow null (e.g., organizations?: { organizationId: string; agentId: string }[]
| null) so both relations follow the same optional/nullable pattern as
databases.
- Around line 24-35: The join table organizationAgent (organization_agents) is
missing createdAt/updatedAt columns used elsewhere; update the pgTable
definition for organizationAgent to add timestamp columns (e.g., createdAt and
updatedAt with appropriate defaults/precision) or use the shared ...timestamps
helper if available, ensuring the new columns are declared alongside
organizationId and agentId and remain compatible with the existing unique()
index and cascade references.

In `@src/db/services/agent.ts`:
- Around line 22-33: The query that builds the agent list currently only joins
organizationAgent and thus omits agents where agent.organizationId ===
organizationId; update the WHERE logic in the query (the block using
organizationAgent, agent, organizationId) to include agents that are either
linked via the organization_agents join OR directly owned via
agent.organizationId equals organizationId (i.e., add an OR condition that
checks eq(agent.organizationId, organizationId)). Also relax the archive check
on agent.isArchived from eq(agent.isArchived, false) to exclude only true (e.g.,
not(eq(agent.isArchived, true))) so NULL legacy rows are included.

In `@src/db/services/database.ts`:
- Around line 8-31: The current implementation fetches databases via
db.query.database.findMany into availableDatabases and then filters by
organization in memory; instead push the organization predicate into the
database query by extending the where clause passed to
db.query.database.findMany (the same call that currently uses isNull/eq on
projectId) to also require that the related agent either has
agent.organizationId equal to organizationId or that an agent.organizations
entry exists with organizationId equal to organizationId (use a join/EXISTS
style predicate), so only eligible Database rows are returned and you can remove
the post-query Array.filter that checks agent and agent.organizations; update
the where builder in db.query.database.findMany (same call that returns
availableDatabases) to include this agent/organizations predicate while keeping
the existing projectId logic and with: settings for agent, project, backups,
restorations.

In `@src/features/agents/agents.action.ts`:
- Around line 28-35: The two-step write creating the agent and then inserting
into organizationAgent must be atomic; wrap the db.insert to
drizzleDb.schemas.agent and the subsequent insert to
drizzleDb.schemas.organizationAgent in a single transaction so that either both
commits or neither do. Locate the code that uses
db.insert(...).values({...parsedInput.data, slug, organizationId:
parsedInput.organizationId}).returning() (symbols: createdAgent, parsedInput,
drizzleDb.schemas.agent, drizzleDb.schemas.organizationAgent) and replace the
separate inserts with a transactional block using the project's Drizzle
transaction API (begin/transaction/tx method) so that the agent creation and
organizationAgent join insert execute under the same transaction and errors roll
back both operations.

In `@src/features/agents/components/agent-organizations.action.ts`:
- Around line 49-55: Replace the sequential single-row inserts inside the loop
that iterates organizationsToAdd with a single batch insert: build an array of
value objects using organizationsToAdd and agentId and call
db.insert(drizzleDb.schemas.organizationAgent).values(batchArray) once instead
of awaiting each insert inside the for loop (refer to organizationsToAdd,
agentId, and drizzleDb.schemas.organizationAgent to locate the code).
- Around line 20-106: The handler in agent-organizations.action.ts performs
multiple DB changes (inserts to drizzleDb.schemas.organizationAgent, delete from
organizationAgent, update drizzleDb.schemas.database, and deletes from
retentionPolicy, alertPolicy, storagePolicy) without a transaction; wrap all
related operations (the loop that inserts organizationAgent rows, the delete of
organizationAgent, the subsequent query of organization, the update of database
and deletes of retentionPolicy/alertPolicy/storagePolicy) inside a single
Drizzle transaction so they either all commit or all rollback on error; use the
transaction object (e.g., tx.insert, tx.delete, tx.update, tx.query.*) instead
of the global db for those calls and ensure errors rethrow or return a failure
response so the transaction can roll back.

In `@src/features/agents/components/agent-organizations.schema.ts`:
- Around line 3-4: AgentOrganizationSchema currently accepts any string for the
organizations array; change the validation to require UUIDs by updating the
organizations element schema in AgentOrganizationSchema to use z.string().uuid()
(or z.string().uuid({ version: "..." }) if a specific UUID version is required)
so malformed IDs are rejected at form validation instead of failing later in the
write path.

In `@src/features/agents/components/agent.dialog.tsx`:
- Around line 62-85: The Organizations tab currently mounts
AgentOrganisationForm even when agent is undefined causing create flows to
submit an empty id; change the adminView Tabs so the TabsTrigger and TabsContent
for value="organizations" are only rendered when agent?.id exists (or otherwise
disable/hide the "organizations" trigger until after the first save). Locate the
adminView branch that renders Tabs, the TabsTrigger with value="organizations"
and the TabsContent that mounts AgentOrganisationForm (which reads
defaultValues?.id) and wrap those pieces in a conditional that checks agent?.id
(or make agent required for adminView) so the form is not mounted for new
agents.

In `@src/lib/auth/auth.ts`:
- Line 33: Replace the "//TODO: capture errors in a monitoring service" comment
in src/lib/auth/auth.ts by initializing a monitoring client (e.g., Sentry) at
module load and calling its capture method inside the auth error handling paths:
import and call Sentry.init(...) (using an env var like SENTRY_DSN) near the top
of the module, then in each catch block or error handler in this file (the
functions that perform authentication/verification) call
Sentry.captureException(err) with contextual tags (user id, function name) and
rethrow or return the appropriate error; ensure the monitoring init is guarded
by config so it can be disabled in tests and that sensitive data is not sent.

---

Outside diff comments:
In
`@src/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent.tsx`:
- Around line 22-27: The onSuccess handler currently always calls
router.push("/dashboard/agents") after delete; change it to choose the
post-delete redirect based on props.organizationId so org-scoped deletes return
to the organization UI. In the onSuccess of the mutationFn (where
deleteAgentAction is called and props.agentId/props.organizationId are
available), replace the hardcoded router.push("/dashboard/agents") with a
conditional that pushes the organization-specific route when
props.organizationId is set (e.g. the org agents list) and falls back to the
global "/dashboard/agents" otherwise; update any tests/consumers that assume the
previous hardcoded redirect if needed.

In `@src/components/wrappers/dashboard/organization/tabs/organization-tabs.tsx`:
- Around line 84-111: The Agents tab is currently shown/hidden under the wrong
permission; update organization-tabs.tsx to guard the Agents tab with the
dedicated canManageAgents flag: wrap (or conditionally render) the TabsTrigger
with value="agents" and the corresponding TabsContent value="agents" (which
renders OrganizationAgentsTab with prop agents) behind canManageAgents instead
of the notification/storage permission. Ensure both the tab trigger and its
content check canManageAgents so visibility is consistent.

In `@src/features/agents/components/agent.form.tsx`:
- Around line 67-70: The fallback redirect in the success handler currently
always does router.push(`/dashboard/agents/${data.id}`) which 404s for org-owned
agents; update the fallback in the component (the props.onSuccess branch in
agent.form.tsx) to check data.organizationId and, if present, push to the
org-scoped agent route (include data.organizationId and data.id in the URL),
otherwise keep the existing admin route; modify the code around props.onSuccess
and router.push to choose the route based on data.organizationId.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: acf3ce39-9fe4-4c91-8d8c-1c52a956b92d

📥 Commits

Reviewing files that changed from the base of the PR and between a620d7a and ae88cbb.

📒 Files selected for processing (41)
  • app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx
  • app/(customer)/dashboard/(admin)/agents/page.tsx
  • app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx
  • app/(customer)/dashboard/(organization)/settings/agents/[agentId]/page.tsx
  • app/(customer)/dashboard/(organization)/settings/agents/page.tsx
  • app/(customer)/dashboard/(organization)/settings/page.tsx
  • app/api/agent/[agentId]/backup/upload/status/route.ts
  • app/api/agent/[agentId]/restore/route.ts
  • app/api/agent/[agentId]/status/helpers.ts
  • src/components/wrappers/common/bread-crumbs/bread-crumbs.tsx
  • src/components/wrappers/dashboard/admin/settings/email/email-form/email-form.action.ts
  • src/components/wrappers/dashboard/admin/settings/notification/settings-notification.action.ts
  • src/components/wrappers/dashboard/admin/settings/storage/settings-storage.action.ts
  • src/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.action.ts
  • src/components/wrappers/dashboard/agent/agent-card/agent-card.tsx
  • src/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent.tsx
  • src/components/wrappers/dashboard/agent/button-delete-agent/delete-agent.action.ts
  • src/components/wrappers/dashboard/database/import/upload-backup-zone.tsx
  • src/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-agents-tab.tsx
  • src/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-notifiers-tab.tsx
  • src/components/wrappers/dashboard/organization/tabs/organization-tabs.tsx
  • src/db/index.ts
  • src/db/migrations/0049_chief_terrax.sql
  • src/db/migrations/meta/0049_snapshot.json
  • src/db/migrations/meta/_journal.json
  • src/db/schema/07_database.ts
  • src/db/schema/08_agent.ts
  • src/db/schema/12_storage-channel.ts
  • src/db/schema/13_storage-policy.ts
  • src/db/services/agent.ts
  • src/db/services/database.ts
  • src/features/agents/agents.action.ts
  • src/features/agents/agents.schema.ts
  • src/features/agents/components/agent-organizations.action.ts
  • src/features/agents/components/agent-organizations.form.tsx
  • src/features/agents/components/agent-organizations.schema.ts
  • src/features/agents/components/agent.dialog.tsx
  • src/features/agents/components/agent.form.tsx
  • src/lib/acl/organization-acl.ts
  • src/lib/auth/auth.ts
  • src/utils/init.ts
💤 Files with no reviewable changes (3)
  • src/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-notifiers-tab.tsx
  • src/components/wrappers/dashboard/database/import/upload-backup-zone.tsx
  • src/db/schema/12_storage-channel.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-test
🔇 Additional comments (17)
src/utils/init.ts (1)

107-108: Good timestamp consistency on settings update.

Using withUpdatedAt here keeps updatedAt in sync when defaultStorageChannelId is initialized.

src/components/wrappers/dashboard/admin/settings/email/email-form/email-form.action.ts (1)

22-24: Looks correct.

The .set(...) payload now updates timestamps consistently while preserving the same settings fields.

app/api/agent/[agentId]/backup/upload/status/route.ts (1)

73-76: Timestamp-wrapped status promotion looks good.

Applying withUpdatedAt on backup success transition is consistent with the rest of the mutation paths.

src/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.action.ts (1)

23-23: withUpdatedAt usage here is good.

This keeps S3-related settings writes aligned with timestamp update conventions.

app/api/agent/[agentId]/status/helpers.ts (1)

186-187: Restoration status transitions are now consistently timestamped.

The failed and ongoing updates now correctly carry updatedAt in all branches.

Also applies to: 197-198, 204-205

app/api/agent/[agentId]/restore/route.ts (1)

66-67: Good update payload change.

Wrapping restoration status updates with withUpdatedAt keeps this route consistent with other status mutation handlers.

src/components/wrappers/dashboard/admin/settings/storage/settings-storage.action.ts (1)

26-30: This is a solid change.

Explicit column mapping is preserved and updatedAt is now updated atomically with the storage settings fields.

src/components/wrappers/dashboard/admin/settings/notification/settings-notification.action.ts (1)

28-30: Nice consistency improvement.

defaultNotificationChannelId updates now also refresh updatedAt, matching the rest of the settings actions.

src/features/agents/agents.schema.ts (1)

6-6: Whitespace-only change; no functional impact.
No schema behavior/type impact from this segment.

src/db/index.ts (1)

29-29: No functional effect from this import change in current state.
This segment does not introduce behavior changes by itself.

src/db/schema/13_storage-policy.ts (1)

4-4: Import cleanup looks correct.
This keeps only the symbol actually used by the schema/relation definitions.

src/components/wrappers/common/bread-crumbs/bread-crumbs.tsx (1)

47-47: Allowing “settings” as a navigable breadcrumb is consistent with the new flow.
This change is coherent with organization settings navigation.

src/components/wrappers/dashboard/agent/agent-card/agent-card.tsx (1)

18-18: Route switching by organizationView is clean and straightforward.
Good extension of AgentCard for shared usage across admin/org contexts.

Also applies to: 38-38

src/lib/acl/organization-acl.ts (1)

10-10: canManageAgents integration is consistent with the ACL pattern.
Type and computation are aligned and preserve existing role semantics.

Also applies to: 35-35

app/(customer)/dashboard/(organization)/settings/page.tsx (1)

22-22: Organization agents data wiring is correct.
Fetching organization-scoped agents and passing them to tabs aligns with the new settings surface.

Also applies to: 39-39, 92-92

src/features/agents/components/agent-organizations.form.tsx (1)

27-35: Nice default-value mapping for shared agents.

Prefilling the form from defaultValues.organizations keeps the multiselect aligned with the join-table shape and makes editing existing assignments straightforward.

src/features/agents/components/agent-organizations.action.ts (1)

88-98: Explicit policy deletions may be redundant due to cascade constraints.

Per the schema (context snippet 4), retentionPolicy, alertPolicy, and storagePolicy all have onDelete: 'cascade' referencing database.id. If you delete the database row, these policies are automatically removed.

However, here you're only updating the database rows (setting projectId to null), not deleting them, so explicit deletes are necessary. Consider adding a comment explaining this to prevent future confusion.

@Portabase Portabase deleted a comment from coderabbitai bot Apr 9, 2026
@Portabase Portabase deleted a comment from coderabbitai bot Apr 9, 2026
@Portabase Portabase deleted a comment from coderabbitai bot Apr 9, 2026
@Portabase Portabase deleted a comment from coderabbitai bot Apr 9, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/`(customer)/dashboard/(organization)/settings/page.tsx:
- Around line 81-96: The server action that performs organization deletion (the
handler invoked by DeleteOrganizationButton) must enforce the same "no projects"
rule as the UI: before calling the deletion, query the organization's projects
(e.g., via organizationSlug) and if any exist return/throw an error (HTTP 400 or
thrown Error) preventing deletion; update the server action (the
deleteOrganization / handleDeleteOrganization function used by
DeleteOrganizationButton) to check projects.length > 0 (or a count query) and
reject with a clear message so direct calls cannot bypass the UI constraint.

In
`@src/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-agents-tab.tsx`:
- Around line 10-13: The prop type for OrganizationAgentsTabProps is incorrect:
change the agents property from Agent[] to the richer AgentWith[] (the type
expected by AgentCard) or ensure the data passed into OrganizationAgentsTab (and
any data-fetching code) includes the organizations relation so AgentCard's data:
AgentWith requirement is satisfied; update OrganizationAgentsTabProps to agents:
AgentWith[] and adjust upstream fetch/transform logic so each agent includes an
organizations array used by AgentCard.

In `@src/db/migrations/0050_dark_saracen.sql`:
- Around line 10-30: The migration currently backfills organization_agents only
via projects→databases→agent_id; also insert rows for agents that have a direct
agents.organization_id. Update the migration (0050_dark_saracen.sql) to also
INSERT INTO organization_agents (organization_id, agent_id) SELECT
organization_id, id FROM agents WHERE organization_id IS NOT NULL ON CONFLICT
(organization_id, agent_id) DO NOTHING (or add an equivalent loop that iterates
agents with non-null organization_id and inserts into organization_agents),
ensuring you reference the agents table, agents.organization_id, agents.id
(agent_id) and use the same ON CONFLICT clause as the existing inserts.

In `@src/db/schema/08_agent.ts`:
- Line 18: The agent ownership field agent.organizationId becomes stale when an
agent is removed from its owner org because delete-agent.action.ts currently
only deletes the organizationAgent join row; update the deletion logic to either
clear agent.organizationId (set to null) whenever the owner organization is
removed from an agent, or enforce admin-only deletion that removes all
organizationAgent rows and then deletes the agent record; specifically modify
the code in delete-agent.action.ts (and any service that removes
organizationAgent rows) to check if the removed organizationId equals
agent.organizationId and if so update the Agent record to nullify organizationId
(or alternatively cascade-remove all organizationAgent rows and delete Agent
only for admins), and ensure the access check in
settings/agents/[agentId]/page.tsx remains valid by keeping agent.organizationId
consistent with organizationAgent entries.

In `@src/db/services/agent.ts`:
- Around line 6-42: The function getOrganizationAgents currently casts the query
result to Agent[] despite selecting an aggregated databases field; update the
function signature and return type to AgentWithDatabases[] (or a matching type
that includes the databases array), import AgentWithDatabases from the schema
(e.g. from src/db/schema/08_agent.ts), remove the unsafe as unknown as Agent[]
cast, and ensure the sql<> aggregation for databases maps to Database[] so the
DB query result matches the new AgentWithDatabases return type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: baa85dc0-bfe2-4bce-a9a1-cedadfdb9a8d

📥 Commits

Reviewing files that changed from the base of the PR and between ae88cbb and 717f9e3.

📒 Files selected for processing (13)
  • app/(customer)/dashboard/(organization)/settings/agents/[agentId]/page.tsx
  • app/(customer)/dashboard/(organization)/settings/page.tsx
  • src/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent.tsx
  • src/components/wrappers/dashboard/organization/delete-organization-button.tsx
  • src/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-agents-tab.tsx
  • src/db/migrations/0050_dark_saracen.sql
  • src/db/migrations/0051_young_senator_kelly.sql
  • src/db/migrations/meta/0050_snapshot.json
  • src/db/migrations/meta/0051_snapshot.json
  • src/db/migrations/meta/_journal.json
  • src/db/schema/08_agent.ts
  • src/db/services/agent.ts
  • src/features/agents/components/agent.form.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-test
🔇 Additional comments (8)
src/db/migrations/0051_young_senator_kelly.sql (1)

1-3: > Likely an incorrect or invalid review comment.

src/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-agents-tab.tsx (1)

2-3: Unused imports and state already flagged.

The unused EmptyStatePlaceholder import, useState import, and isAddModalOpen/setIsAddModalOpen state have been flagged in previous reviews. Please address these by either removing them or implementing the intended modal functionality.

Also applies to: 19-19

app/(customer)/dashboard/(organization)/settings/agents/[agentId]/page.tsx (3)

63-63: isOwned ownership check issue already flagged.

This was previously flagged: isOwned only checks if agent.organizationId is truthy, not whether the current organization owns the agent. Should compare: agent.organizationId === organization.id.


82-82: Type mismatch with null already flagged.

This was previously flagged: passing null where string | undefined is expected. Since organization is verified on line 32, organization.id will always be defined here. Use organization.id directly.


55-61: LGTM!

The access control logic correctly checks both direct ownership (agent.organizationId === organization.id) and shared access via the join table (agent.organizations.some(...)). This properly supports the dual-ownership model.

src/db/schema/08_agent.ts (2)

59-65: AgentWith type inconsistency already flagged.

Previous review noted that databases is optional (databases?: Database[] | null) while organizations is required. This inconsistency can cause type errors when loading agents without eager-loading organizations.


23-36: LGTM on join table implementation!

The organizationAgent join table is well-structured:

  • Proper cascade deletes on both foreign keys
  • Unique constraint prevents duplicate associations
  • Timestamps added (addressing prior feedback)
  • Relations correctly defined for bidirectional queries

Also applies to: 44-45

src/db/migrations/meta/0051_snapshot.json (1)

1673-1752: LGTM!

The organization_agents join table is correctly defined with:

  • Foreign keys to organization and agents with cascade deletes
  • Unique constraint on (organization_id, agent_id) preventing duplicate associations
  • Timestamp columns for auditing

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
src/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-agents-tab.tsx (1)

8-11: ⚠️ Potential issue | 🟠 Major

Align agents prop type with AgentCard’s required data shape.

At Line 10, agents is typed as Agent[], but this component passes each item to AgentCard at Line 31. If AgentCard still requires a richer relation type (for example including organizations), this is a type/runtime contract break.

🔧 Proposed fix
-import {Agent} from "@/db/schema/08_agent";
+import {AgentWith} from "@/db/schema/08_agent";

 export type OrganizationAgentsTabProps = {
     organization: OrganizationWithMembers;
-    agents: Agent[];
+    agents: AgentWith[];
 };
#!/bin/bash
# Verify AgentCard data contract and this tab prop contract.
rg -n -C3 'type\s+AgentCardProps|interface\s+AgentCardProps|data:\s*' src/components/wrappers/dashboard/agent/agent-card/agent-card.tsx
rg -n -C3 'export type OrganizationAgentsTabProps|agents:\s*' src/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-agents-tab/organization-agents-tab.tsx
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-agents-tab.tsx`
around lines 8 - 11, The agents prop in OrganizationAgentsTabProps is declared
as Agent[] but each item is passed into AgentCard (see AgentCard
component/AgentCardProps), so update OrganizationAgentsTabProps (and its agents
usage) to match the exact data shape AgentCard expects (for example
AgentWithRelations or Agent & { organizations: Organization[] } if AgentCard
requires organizations), or change AgentCard to accept the narrower Agent type;
ensure the type name you choose (e.g., AgentWithOrganizations or
AgentCardProps['data']) replaces Agent[] in OrganizationAgentsTabProps and
adjust any map/props calls inside organization-agents-tab to satisfy the new
type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/`(customer)/dashboard/(organization)/projects/[projectId]/page.tsx:
- Line 58: The call to getOrganizationAvailableDatabases uses the stale
auth-derived organization.id; change it to use the freshly fetched DB record
org.id so the service filters against the same DB state—update the invocation
that assigns availableDatabases to pass org.id (keep proj.id unchanged) to match
the earlier DB lookup for org and ensure consistent organization filtering.

In
`@src/components/wrappers/auth/login/forgot-password-form/forgot-password.actions.ts`:
- Line 9: The TODO indicates the password-reset action in this file returns
success: true without actually sending reset instructions; update the
forgot-password action (the exported handler in forgot-password.actions.ts) to
either (A) re-enable and call the actual send/reset delivery logic (e.g., invoke
the existing sendResetEmail/sendPasswordReset or Auth.forgotPassword helper) and
await it, catching and logging errors, then return success: true only on
delivery success, or (B) if delivery is not implemented yet, change the return
to success: false and include an explanatory error message so callers are not
misled; ensure you reference and use the existing request input
(email/identifier) and the local function names used in this file, and handle
exceptions to avoid returning a false success on failure.

In `@src/features/agents/components/agent-organizations.action.ts`:
- Line 112: Fix the grammar in the success notification by changing the message
string "Agent organizations has been successfully updated." to use plural
agreement: "Agent organizations have been successfully updated."; locate the
message property in src/features/agents/components/agent-organizations.action.ts
(the object containing the message key) and update its value accordingly.
- Around line 14-17: The server-side action schema in
agent-organizations.action.ts currently validates id as z.string(), which is
weaker than the companion AgentOrganizationSchema (which uses
z.string().uuid()); update the action's z.object (the one with keys data and id)
to validate id with z.string().uuid() so IDs are enforced as UUIDs, and ensure
any array items in data match the expected type in AgentOrganizationSchema if
they represent IDs as well.
- Around line 72-98: The current database queries use projectIds only and thus
affect databases owned by other agents; update db.query.database.findMany to
also filter by the current agentId (e.g., include a condition on
drizzleDb.schemas.database.agentId or the column representing agent ownership)
so databaseIds only contains databases for this agent, and add the same agentId
condition to the subsequent update and delete statements (the db.update(...) on
drizzleDb.schemas.database and the db.delete(...) calls for retentionPolicy,
alertPolicy, storagePolicy) so each WHERE includes both inArray(...projectIds)
and the agentId equality check.

---

Duplicate comments:
In
`@src/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-agents-tab.tsx`:
- Around line 8-11: The agents prop in OrganizationAgentsTabProps is declared as
Agent[] but each item is passed into AgentCard (see AgentCard
component/AgentCardProps), so update OrganizationAgentsTabProps (and its agents
usage) to match the exact data shape AgentCard expects (for example
AgentWithRelations or Agent & { organizations: Organization[] } if AgentCard
requires organizations), or change AgentCard to accept the narrower Agent type;
ensure the type name you choose (e.g., AgentWithOrganizations or
AgentCardProps['data']) replaces Agent[] in OrganizationAgentsTabProps and
adjust any map/props calls inside organization-agents-tab to satisfy the new
type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 97c0f202-c40f-4cad-8be7-9e387a8dd064

📥 Commits

Reviewing files that changed from the base of the PR and between 717f9e3 and 4ee9761.

📒 Files selected for processing (6)
  • app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx
  • src/components/wrappers/auth/login/forgot-password-form/forgot-password.actions.ts
  • src/components/wrappers/dashboard/admin/channels/organization/channels-organization.schema.ts
  • src/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-agents-tab.tsx
  • src/features/agents/components/agent-organizations.action.ts
  • src/features/agents/components/agent-organizations.schema.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-test
🔇 Additional comments (3)
src/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-agents-tab.tsx (1)

22-34: Conditional trigger/state rendering is clean and consistent.

The hasAgent split is clear: create CTA when populated (Line 28) and empty-state CTA when no agents exist (Line 33). This is good UX for organization-scoped agent management.

src/components/wrappers/dashboard/admin/channels/organization/channels-organization.schema.ts (1)

3-5: LGTM!

Adding UUID validation to the organizations array elements ensures malformed IDs are rejected at validation time rather than failing later in the data layer. This is consistent with the newly introduced AgentOrganizationSchema.

src/features/agents/components/agent-organizations.schema.ts (1)

1-7: LGTM!

The schema correctly validates organization IDs as UUIDs, which addresses the prior review feedback. This ensures malformed IDs are rejected at form validation.

import {action} from "@/lib/safe-actions/actions";

//todo: to be continued...
//TODO: to be continued...
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

TODO marks an incomplete password-reset path that currently returns success without sending reset instructions.

This should not ship as-is: the action can return success: true even though reset delivery is not executed (send logic is commented out), which breaks recovery UX and can mislead clients.

Proposed fix direction
-//TODO: to be continued...
+// Implemented: generate verification token and send reset email before returning success.
-            // await (
-            //     await auth.$context
-            // ).options.emailAndPassword
-            //     .sendResetPassword(...)
+            // 1) Create/store verification token with expiry
+            // 2) Build reset URL from `parsedInput.redirectTo` (validated allowlist)
+            // 3) Call sendResetPassword(...)
+            // 4) Return success only after send succeeds
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/components/wrappers/auth/login/forgot-password-form/forgot-password.actions.ts`
at line 9, The TODO indicates the password-reset action in this file returns
success: true without actually sending reset instructions; update the
forgot-password action (the exported handler in forgot-password.actions.ts) to
either (A) re-enable and call the actual send/reset delivery logic (e.g., invoke
the existing sendResetEmail/sendPasswordReset or Auth.forgotPassword helper) and
await it, catching and logging errors, then return success: true only on
delivery success, or (B) if delivery is not implemented yet, change the return
to success: false and include an explanatory error message so callers are not
misled; ensure you reference and use the existing request input
(email/identifier) and the local function names used in this file, and handle
exceptions to avoid returning a false success on failure.

Comment on lines +72 to +98
const databases = await db.query.database.findMany({
where: (db, { inArray }) => inArray(db.projectId, projectIds),
columns: { id: true }
});

const databaseIds = databases.map(d => d.id);

await db
.update(drizzleDb.schemas.database)
.set(withUpdatedAt({
backupPolicy: null,
projectId: null
}))
.where(inArray(drizzleDb.schemas.database.projectId, projectIds))
.execute();

await db.delete(drizzleDb.schemas.retentionPolicy)
.where(inArray(drizzleDb.schemas.retentionPolicy.databaseId, databaseIds))
.execute();

await db.delete(drizzleDb.schemas.alertPolicy)
.where(inArray(drizzleDb.schemas.alertPolicy.databaseId, databaseIds))
.execute();

await db.delete(drizzleDb.schemas.storagePolicy)
.where(inArray(drizzleDb.schemas.storagePolicy.databaseId, databaseIds))
.execute();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Database operations affect other agents' databases.

The query at lines 72-75 finds databases by projectId without filtering by the current agentId. This means if multiple agents have databases in the same project, removing an organization from agent A will incorrectly clear backupPolicy/projectId and delete policies for databases belonging to other agents.

🐛 Proposed fix to scope operations to the current agent
                 if (projectIds.length > 0) {
                     const databases = await db.query.database.findMany({
-                        where: (db, { inArray }) => inArray(db.projectId, projectIds),
+                        where: (db, { inArray, and, eq }) => and(
+                            inArray(db.projectId, projectIds),
+                            eq(db.agentId, agentId)
+                        ),
                         columns: { id: true }
                     });

                     const databaseIds = databases.map(d => d.id);

+                    if (databaseIds.length === 0) {
+                        // No databases to update for this agent
+                        return;
+                    }
+
                     await db
                         .update(drizzleDb.schemas.database)
                         .set(withUpdatedAt({
                             backupPolicy: null,
                             projectId: null
                         }))
-                        .where(inArray(drizzleDb.schemas.database.projectId, projectIds))
+                        .where(inArray(drizzleDb.schemas.database.id, databaseIds))
                         .execute();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const databases = await db.query.database.findMany({
where: (db, { inArray }) => inArray(db.projectId, projectIds),
columns: { id: true }
});
const databaseIds = databases.map(d => d.id);
await db
.update(drizzleDb.schemas.database)
.set(withUpdatedAt({
backupPolicy: null,
projectId: null
}))
.where(inArray(drizzleDb.schemas.database.projectId, projectIds))
.execute();
await db.delete(drizzleDb.schemas.retentionPolicy)
.where(inArray(drizzleDb.schemas.retentionPolicy.databaseId, databaseIds))
.execute();
await db.delete(drizzleDb.schemas.alertPolicy)
.where(inArray(drizzleDb.schemas.alertPolicy.databaseId, databaseIds))
.execute();
await db.delete(drizzleDb.schemas.storagePolicy)
.where(inArray(drizzleDb.schemas.storagePolicy.databaseId, databaseIds))
.execute();
const databases = await db.query.database.findMany({
where: (db, { inArray, and, eq }) => and(
inArray(db.projectId, projectIds),
eq(db.agentId, agentId)
),
columns: { id: true }
});
const databaseIds = databases.map(d => d.id);
if (databaseIds.length === 0) {
// No databases to update for this agent
return;
}
await db
.update(drizzleDb.schemas.database)
.set(withUpdatedAt({
backupPolicy: null,
projectId: null
}))
.where(inArray(drizzleDb.schemas.database.id, databaseIds))
.execute();
await db.delete(drizzleDb.schemas.retentionPolicy)
.where(inArray(drizzleDb.schemas.retentionPolicy.databaseId, databaseIds))
.execute();
await db.delete(drizzleDb.schemas.alertPolicy)
.where(inArray(drizzleDb.schemas.alertPolicy.databaseId, databaseIds))
.execute();
await db.delete(drizzleDb.schemas.storagePolicy)
.where(inArray(drizzleDb.schemas.storagePolicy.databaseId, databaseIds))
.execute();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/agents/components/agent-organizations.action.ts` around lines 72
- 98, The current database queries use projectIds only and thus affect databases
owned by other agents; update db.query.database.findMany to also filter by the
current agentId (e.g., include a condition on drizzleDb.schemas.database.agentId
or the column representing agent ownership) so databaseIds only contains
databases for this agent, and add the same agentId condition to the subsequent
update and delete statements (the db.update(...) on drizzleDb.schemas.database
and the db.delete(...) calls for retentionPolicy, alertPolicy, storagePolicy) so
each WHERE includes both inArray(...projectIds) and the agentId equality check.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (5)
app/(customer)/dashboard/(organization)/settings/agents/[agentId]/page.tsx (1)

67-86: ⚠️ Potential issue | 🟠 Major

Only show owner controls when the current organization actually owns the agent.

const isOwned = agent.organizationId is truthy for any org-owned agent. Since hasAccess also grants access through agent.organizations, a linked org can pass the access check here and still see edit/delete controls for an agent owned by some other organization. Compare against organization.id instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(customer)/dashboard/(organization)/settings/agents/[agentId]/page.tsx
around lines 67 - 86, The ownership check uses const isOwned =
agent.organizationId which is truthy for any agent with an owner but doesn't
verify it matches the current organization; change the logic in the page
component to compare agent.organizationId to organization.id (e.g., set isOwned
= agent.organizationId === organization.id) so the edit/delete controls
(AgentDialog, ButtonDeleteAgent) only render when the current organization
actually owns the agent.
app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx (1)

33-49: 🧹 Nitpick | 🔵 Trivial

Move the organizations lookup below the notFound() guards.

Invalid IDs and org-owned agents still pay for organization.findMany({ with: { members: true } }) before this page bails out. Defer that query until after the !agent and isOwnerByAnOrganization checks so the 404 path stays cheap.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(customer)/dashboard/(admin)/agents/[agentId]/page.tsx around lines 33 -
49, The organizations query (the call to db.query.organization.findMany with
with: { members: true }) is executed before the notFound guards; move that query
below the agent existence and ownership checks so invalid agent IDs and
org-owned agents return 404 without running organization.findMany. Concretely:
keep the checks for if (!agent) { notFound(); } and the isOwnerByAnOrganization
= agent.organizationId / if (isOwnerByAnOrganization) { notFound(); }
immediately after loading agent, then run the organizations lookup (the
organization.findMany call that populates organizations) only after those guards
pass.
src/components/wrappers/dashboard/agent/button-delete-agent/delete-agent.action.ts (2)

121-123: ⚠️ Potential issue | 🟡 Minor

Use agentId in the success metadata.

messageParams still emits { projectId: agentId }, which is inconsistent with the rest of this action and will break any consumer expecting agentId.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/components/wrappers/dashboard/agent/button-delete-agent/delete-agent.action.ts`
around lines 121 - 123, The success metadata uses the wrong key; update the
actionSuccess.messageParams to emit agentId instead of projectId so consumers
get the expected identifier. Locate the actionSuccess block (symbol:
actionSuccess) in delete-agent.action.ts and change messageParams from {
projectId: agentId } to { agentId: agentId } (or just { agentId }) so it matches
the rest of the action and downstream consumers.

29-104: ⚠️ Potential issue | 🔴 Critical

Split org detach from global delete, and make each path atomic.

When organizationId is present, this removes one organization_agents row and then still archives the agent globally a few lines later. That lets one org disable a shared agent for every other org, and a failure mid-flow can leave memberships and cleanup half-applied. This should branch into “detach one org” vs “archive everywhere” and run each branch inside a single transaction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/components/wrappers/dashboard/agent/button-delete-agent/delete-agent.action.ts`
around lines 29 - 104, The current flow mixes “detach single org” and “archive
globally” logic; when organizationId is provided you should only remove the
organization-agent link (the db.delete on drizzleDb.schemas.organizationAgent)
inside a single transaction and must NOT run the global agent archiving (the
db.update on drizzleDb.schemas.agent that sets isArchived/slug/deletedAt);
conversely, when organizationIds (or the global delete path) is taken, perform
the full cleanup (collect projectIds, null out database.projectId, delete
retentionPolicy/alertPolicy/storagePolicy, then archive the agent via update on
drizzleDb.schemas.agent) inside one transaction so either all cleanup + archive
succeeds or all rolls back; refactor the branches around organizationId vs
organizationIds to use explicit transactions (the DB transaction helper in your
DB client) and move the agent update into only the global-delete branch.
src/db/services/database.ts (1)

12-39: ⚠️ Potential issue | 🟠 Major

Filter by organization in SQL instead of post-filtering in memory.

This still fetches every candidate database plus its related agent/project/backup/restoration rows, then drops unrelated orgs in JS. On multi-tenant project pages that scales with all tenants and moves cross-org data through the app layer unnecessarily. Push the organization and archived-agent predicates into the findMany query so only eligible rows are loaded.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/services/database.ts` around lines 12 - 39, The current code fetches
all candidate databases then filters by agent.isArchived and organization
membership in memory; change the predicate so db.query.database.findMany only
returns eligible rows by adding agent-related conditions into the where clause:
include a condition that the related agent is not archived (agent.isArchived =
false) and that either agent.organizationId = organizationId OR the
agent.organizations relation contains an entry with organizationId (use the
relational/or filter APIs your query builder exposes), while preserving the
existing projectId logic, with/with includes (agent, project, backups,
restorations) and orderBy; update the where passed to db.query.database.findMany
(the call in availableDatabases) and remove the post-query JS filter on
availableDatabases and agent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/`(customer)/dashboard/(organization)/settings/agents/[agentId]/page.tsx:
- Around line 64-65: Remove the two debug console.log calls that print
"oooooooooooooooooo" and "iciii" along with organization.id from the route's
page.tsx; locate the page component where those console.log(...) calls occur and
delete them (or replace with a proper server-side logger at an appropriate level
if you need audit logging) so organization IDs are not leaked to server logs on
every request.

In `@app/api/agent/`[agentId]/restore/route.ts:
- Line 66: Validate body.status at runtime before casting and use strict
equality when checking for "failed": add a runtime guard (e.g.,
isRestorationStatus(value) or a small switch/allowed-values check) that verifies
body.status is one of the RestorationStatus enum members before calling
withUpdatedAt({ status: body.status as RestorationStatus }) in the restore
route; if invalid, return a 400 error. Also change the loose equality check
(currently body.status == "failed") to strict equality (===) against the
validated RestorationStatus value so only the explicit "failed" status triggers
failure handling. Ensure you update the code paths that write to the DB and send
notifications to rely on the validated status variable.

In `@package.json`:
- Around line 66-67: Update the Zod option objects in your schema helpers to use
the Zod 4 standardized "error" key instead of the deprecated "message" key:
locate the helper functions in src/lib/zod.ts (the schema helpers referenced and
any usages with createSelectSchema) and replace option objects like { message:
"..." } with { error: "..." } for calls such as z.enum(..., {...}),
z.string().email({...}), .min(..., {...}), and .regex(..., {...}); ensure all
helper exports and any createSelectSchema invocations are updated consistently
to avoid deprecation warnings.

In
`@src/components/wrappers/dashboard/agent/button-delete-agent/delete-agent.action.ts`:
- Around line 39-93: The cleanup currently gathers projectIds from organizations
and then operates on all databases in those projects; restrict the scope to only
databases owned by the agent by adding an agent predicate using the agentId
variable: when calling db.query.database.findMany (and when computing
databaseIds) include a predicate that the database.agentId equals agentId (e.g.
combine inArray(db.projectId, projectIds) with eq(db.agentId, agentId)), and
likewise tighten the db.update(drizzleDb.schemas.database).where(...) and each
db.delete(...).where(...) to include the same agentId predicate (or use the
filtered databaseIds from the constrained findMany) so only this agent’s
database rows and their retentionPolicy/alertPolicy/storagePolicy records are
touched.

In `@src/db/migrations/0052_cute_punisher.sql`:
- Line 1: The migration adds a NOT NULL "verified" column to the two_factor
table but omits a DEFAULT, which will fail on existing rows; update the ALTER
TABLE in 0052_cute_punisher.sql to add DEFAULT false (so existing rows get
false) and then update the Drizzle schema's two_factor column definition (the
"verified" column in the table/schema declaration in 02_user.ts) to include
.default(false) so the runtime schema matches the migration.

---

Duplicate comments:
In `@app/`(customer)/dashboard/(admin)/agents/[agentId]/page.tsx:
- Around line 33-49: The organizations query (the call to
db.query.organization.findMany with with: { members: true }) is executed before
the notFound guards; move that query below the agent existence and ownership
checks so invalid agent IDs and org-owned agents return 404 without running
organization.findMany. Concretely: keep the checks for if (!agent) { notFound();
} and the isOwnerByAnOrganization = agent.organizationId / if
(isOwnerByAnOrganization) { notFound(); } immediately after loading agent, then
run the organizations lookup (the organization.findMany call that populates
organizations) only after those guards pass.

In `@app/`(customer)/dashboard/(organization)/settings/agents/[agentId]/page.tsx:
- Around line 67-86: The ownership check uses const isOwned =
agent.organizationId which is truthy for any agent with an owner but doesn't
verify it matches the current organization; change the logic in the page
component to compare agent.organizationId to organization.id (e.g., set isOwned
= agent.organizationId === organization.id) so the edit/delete controls
(AgentDialog, ButtonDeleteAgent) only render when the current organization
actually owns the agent.

In
`@src/components/wrappers/dashboard/agent/button-delete-agent/delete-agent.action.ts`:
- Around line 121-123: The success metadata uses the wrong key; update the
actionSuccess.messageParams to emit agentId instead of projectId so consumers
get the expected identifier. Locate the actionSuccess block (symbol:
actionSuccess) in delete-agent.action.ts and change messageParams from {
projectId: agentId } to { agentId: agentId } (or just { agentId }) so it matches
the rest of the action and downstream consumers.
- Around line 29-104: The current flow mixes “detach single org” and “archive
globally” logic; when organizationId is provided you should only remove the
organization-agent link (the db.delete on drizzleDb.schemas.organizationAgent)
inside a single transaction and must NOT run the global agent archiving (the
db.update on drizzleDb.schemas.agent that sets isArchived/slug/deletedAt);
conversely, when organizationIds (or the global delete path) is taken, perform
the full cleanup (collect projectIds, null out database.projectId, delete
retentionPolicy/alertPolicy/storagePolicy, then archive the agent via update on
drizzleDb.schemas.agent) inside one transaction so either all cleanup + archive
succeeds or all rolls back; refactor the branches around organizationId vs
organizationIds to use explicit transactions (the DB transaction helper in your
DB client) and move the agent update into only the global-delete branch.

In `@src/db/services/database.ts`:
- Around line 12-39: The current code fetches all candidate databases then
filters by agent.isArchived and organization membership in memory; change the
predicate so db.query.database.findMany only returns eligible rows by adding
agent-related conditions into the where clause: include a condition that the
related agent is not archived (agent.isArchived = false) and that either
agent.organizationId = organizationId OR the agent.organizations relation
contains an entry with organizationId (use the relational/or filter APIs your
query builder exposes), while preserving the existing projectId logic, with/with
includes (agent, project, backups, restorations) and orderBy; update the where
passed to db.query.database.findMany (the call in availableDatabases) and remove
the post-query JS filter on availableDatabases and agent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4e756f06-1b5c-4d7d-8554-f68d84044575

📥 Commits

Reviewing files that changed from the base of the PR and between 4ee9761 and 4f503fb.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • Makefile
  • app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx
  • app/(customer)/dashboard/(organization)/projects/page.tsx
  • app/(customer)/dashboard/(organization)/settings/agents/[agentId]/page.tsx
  • app/api/agent/[agentId]/backup/helpers.ts
  • app/api/agent/[agentId]/restore/route.ts
  • app/api/agent/[agentId]/status/route.ts
  • package.json
  • src/components/wrappers/common/empty-state-placeholder.tsx
  • src/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent.tsx
  • src/components/wrappers/dashboard/agent/button-delete-agent/delete-agent.action.ts
  • src/db/migrations/0052_cute_punisher.sql
  • src/db/migrations/meta/0052_snapshot.json
  • src/db/migrations/meta/_journal.json
  • src/db/schema/02_user.ts
  • src/db/services/database.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-test
🔇 Additional comments (7)
Makefile (1)

19-19: Good improvement: absolute bind-mount path is safer for Docker Compose runs.

Line 19’s switch to $$(pwd)/... makes the mount path resolution more robust than a relative path.

app/api/agent/[agentId]/backup/helpers.ts (1)

2-16: Archived-agent guard is correctly enforced in agent lookup.

At Line 15, adding agent.isArchived = false to the agent.id predicate makes the middleware treat archived agents as not found while preserving the existing 404 flow.

app/api/agent/[agentId]/status/route.ts (1)

6-50: Status endpoint lookup now correctly excludes archived agents.

Line 49’s combined predicate is a solid hardening step and aligns existence checks with active-agent semantics.

app/api/agent/[agentId]/restore/route.ts (1)

8-39: Restore route correctly enforces active-agent lookup.

Line 38’s agent.id AND isArchived = false condition is consistent with the archived-agent handling introduced in related routes.

src/db/migrations/meta/_journal.json (1)

347-375: LGTM!

The new migration journal entries for migrations 0049–0052 are correctly formatted and sequential. Timestamps are in increasing order, and the structure matches existing entries.

src/db/migrations/meta/0052_snapshot.json (1)

555-612: Snapshot correctly reflects the schema changes.

The two_factor table definition in the snapshot accurately captures the new verified column as boolean with notNull: true. This auto-generated snapshot is consistent with both the Drizzle schema and migration SQL.

Note: If the migration is updated to include a DEFAULT false, regenerate this snapshot to include the default value.

src/db/schema/02_user.ts (1)

96-104: No changes needed; the schema is correctly aligned with better-auth's twoFactor plugin.

The verified column does not require a .default(false) because better-auth's twoFactor plugin explicitly handles initialization of the verified field when creating records. The schema and migration are already properly configured.

			> Likely an incorrect or invalid review comment.

Comment on lines +66 to +67
"drizzle-orm": "0.45.2",
"drizzle-zod": "0.8.3",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

# Check if the referenced files exist and inspect their content
cat -n src/lib/zod.ts 2>/dev/null | head -20

Repository: Portabase/portabase

Length of output: 952


🏁 Script executed:

# Also check the schema files mentioned
cat -n src/db/schema/07_database.ts 2>/dev/null | head -15
cat -n src/db/schema/08_agent.ts 2>/dev/null | head -15

Repository: Portabase/portabase

Length of output: 1745


🏁 Script executed:

# Check line 105 in package.json
sed -n '100,110p' package.json

Repository: Portabase/portabase

Length of output: 342


🌐 Web query:

Zod 4.3.6 API breaking changes from Zod 3 message vs error parameter

💡 Result:

In Zod v4 (including 4.3.6), error customization was unified under a single error option, and the old message option is still accepted but deprecated. (zod.dev)

What changed vs Zod 3

  • Per-check message

    • Zod 3: z.string().min(5, { message: "Too short" })
    • Zod 4: z.string().min(5, { error: "Too short" }) (preferred) (zod.dev)
  • Type-level messages (required_error, invalid_type_error)

    • Zod 3: z.string({ required_error: "...", invalid_type_error: "..." })
    • Zod 4: z.string({ error: (issue) => issue.input === undefined ? "..." : "..." }) (zod.dev)
  • errorMap replacement

    • Zod 3: errorMap: (issue, ctx) => ({ message: ... })
    • Zod 4: error: (issue) => "..." (can return a string; return undefined to fall back to defaults/next map) (zod.dev)

Citations:


🌐 Web query:

drizzle-zod 0.8.3 compatibility with Zod 4 createSelectSchema

💡 Result:

drizzle-zod@0.8.3 is compatible with Zod 4 for createSelectSchema (and the other create*Schema helpers): its peerDependencies explicitly allow zod ^3.25.0 || ^4.0.0, so a project using zod@4.x satisfies the supported range. [1]

Practical notes:

  • With Zod 4, use the v4 entry when you need to reference z directly in refinements/overrides (e.g. import { z } from "zod/v4"). [2]
  • Drizzle’s docs also note that drizzle-zod is deprecated in favor of the built-in integration (createSelectSchema from drizzle-orm/zod), which is the recommended path going forward. [2]

Sources: [1] drizzle-zod@0.8.3 package.json (peerDependencies) [2] Drizzle ORM docs (drizzle-orm/zod, Zod v4 import, deprecation note)


No breaking changes; migrate helpers to Zod 4 syntax as a best practice.

The upgrade to zod@4.3.6 maintains backward compatibility with the { message: ... } syntax currently used in src/lib/zod.ts (lines 6, 8, 12), and drizzle-zod@0.8.3 explicitly supports both Zod 3 and Zod 4. Your schema helpers and createSelectSchema usage will continue to work without modification.

However, Zod 4 standardizes on the error parameter (the message option is deprecated). Consider updating the helpers to use the new syntax:

z.enum(values, { error: "Field required" })
z.string().email({ error: "Invalid email" })
.min(8, { error: "..." })
.regex(pattern, { error: "..." })

This is optional but recommended to follow Zod 4 best practices and avoid deprecation warnings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 66 - 67, Update the Zod option objects in your
schema helpers to use the Zod 4 standardized "error" key instead of the
deprecated "message" key: locate the helper functions in src/lib/zod.ts (the
schema helpers referenced and any usages with createSelectSchema) and replace
option objects like { message: "..." } with { error: "..." } for calls such as
z.enum(..., {...}), z.string().email({...}), .min(..., {...}), and .regex(...,
{...}); ensure all helper exports and any createSelectSchema invocations are
updated consistently to avoid deprecation warnings.

@RambokDev RambokDev self-assigned this Apr 10, 2026
@RambokDev RambokDev merged commit e38519a into main Apr 10, 2026
4 of 5 checks passed
@RambokDev RambokDev deleted the feat/org-agent branch April 10, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request / clarification: org-scoped agent management without global admin

1 participant