Skip to content

feat(api): Sweep remaining single-org assumptions in OSS#4675

Open
jp-agenta wants to merge 4 commits into
feat/oss-org-creationfrom
feat/oss-singleton-sweep
Open

feat(api): Sweep remaining single-org assumptions in OSS#4675
jp-agenta wants to merge 4 commits into
feat/oss-org-creationfrom
feat/oss-singleton-sweep

Conversation

@jp-agenta

@jp-agenta jp-agenta commented Jun 12, 2026

Copy link
Copy Markdown
Member

Context

After the org-creation PR (#4673), OSS can have multiple organizations, but several code paths still assume exactly one: bare-API-key auth resolves "the" workspace by picking the oldest row, organization/workspace listings return everything in the database instead of the user's memberships, the admin API refuses to delete the legacy oss-default org, and the web sidebar keeps the org switcher disabled outside EE. Sequencing step 4 of the convergence plan.

Changes

API:

  • get_default_workspace_id(user_id) (workspace owned by the user, falling back to their oldest membership) moves from db_manager_ee to OSS db_manager; the is_ee() fork in the auth middleware's bare-key path collapses to one call. The OSS-only get_default_workspace_id_oss() (oldest workspace in the whole DB) and get_oss_organization() are deleted.
  • GET /organizations/ (OSS branch) now lists the requesting user's organizations via the membership join, with each org's workspaces attached, instead of all orgs plus the first workspace in the database. GET /organizations/{id} scopes its workspace and invitation lookups to the requested org rather than request.state.project_id. GET /workspaces/ returns the user's workspaces instead of all of them.
  • Admin API: the oss-default singleton guards are gone (the legacy org is a normal org now; deleting it follows EE semantics), and admin_create_organization always inserts a new row instead of collapsing onto the singleton slug in OSS.

Web (ListOfOrgs.tsx): organization selection is enabled in OSS, the "New organization" item and the owner submenu (transfer/rename/delete) are no longer EE-gated, and the label shows the selected organization's name instead of the hard-coded "Agenta". The Organization settings tab stays EE-gated (its content is domain verification and SSO, which remain EE features).

Tests / notes

  • ruff format and ruff check pass; the edited component passes prettier and introduces no tsc errors.
  • The get_default_workspace_id unit tests move to oss/tests/pytest/unit/services/test_db_manager.py (the function moved OSS-ward, so patching db_manager_ee's engine no longer reached it). The admin org create/delete round-trip returns to the OSS acceptance suite now that the singleton delete guard is gone.
  • Playwright auth bootstrap unified: OSS no longer does the owner-signup + invite + re-login dance; both editions sign up a fresh user directly. AGENTA_TEST_OSS_OWNER_EMAIL remains as an optional fixed-account login for persistent CI deployments.
  • Admin membership delete/swap endpoints stay EE-gated; they were not part of the singleton sweep and can be ungated separately if OSS needs them.
  • Stacked on feat(api): Add OSS org-creation path and EE-shaped signup flow #4673.

What to QA

  • OSS with two orgs (create one via the sidebar): the org switcher lists both, switching works, and each org's sidebar shows only its own workspaces and members.
  • Authenticate with a bare API key (no project/workspace query params): requests resolve to the key owner's workspace, not the deployment's oldest workspace.
  • As org owner, rename / transfer / delete an org from the sidebar submenu in OSS.
  • Regression: a user invited into someone else's org sees that org in the switcher alongside their own.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jun 14, 2026 3:07pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3f66134b-086d-47f1-9044-77ecd264ddc9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(api): Sweep remaining single-org assumptions in OSS' clearly and accurately summarizes the main objective—removing remaining single-organization assumptions from the OSS edition.
Docstring Coverage ✅ Passed Docstring coverage is 90.91% which is sufficient. The required threshold is 60.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description clearly relates to the changeset, detailing removal of single-org assumptions and refactoring workspace/organization resolution across API and web components.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/oss-singleton-sweep

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.

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jun 12, 2026
@dosubot dosubot Bot added Backend enhancement New feature or request Frontend labels Jun 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (3)
api/oss/src/middlewares/auth.py (1)

626-691: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Enforce explicit-scope membership checks in OSS too.

The project_member_exists / workspace_member_exists gate still only runs under is_ee(). After removing OSS singleton assumptions, any authenticated OSS user can pass another organization's project_id or workspace_id in the query string and this middleware will mint a scoped secret token without proving membership in that resource.

api/oss/src/routers/organization_router.py (1)

76-110: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Populate workspaces_by_org before serializing the EE response.

This branch initializes workspaces_by_org = {} and never fills it, so every EE organization now returns workspaces: []. Any client code that relies on Organization.workspaces to keep org/workspace selection in sync loses that relationship.

api/oss/src/routers/workspace_router.py (1)

164-168: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the route's workspace_id for OSS removals.

The OSS branch ignores the path parameter and forwards request.state.project_id to db_manager.remove_user_from_workspace(). After this PR, callers can target a different workspace than their ambient auth scope, so /workspaces/{workspace_id}/users/ can remove memberships/invitations from the wrong workspace context.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fda5621b-f8ce-4759-949b-2ed3c92035e4

📥 Commits

Reviewing files that changed from the base of the PR and between 0c8a441 and ab96500.

📒 Files selected for processing (7)
  • api/ee/src/services/db_manager_ee.py
  • api/oss/src/core/accounts/service.py
  • api/oss/src/middlewares/auth.py
  • api/oss/src/routers/organization_router.py
  • api/oss/src/routers/workspace_router.py
  • api/oss/src/services/db_manager.py
  • web/oss/src/components/Sidebar/components/ListOfOrgs.tsx
💤 Files with no reviewable changes (1)
  • api/oss/src/core/accounts/service.py

Comment on lines +128 to 152
organization_workspaces = [
workspace_db
for workspace_db in await db_manager.get_workspaces()
if str(workspace_db.organization_id) == str(organization_id)
]
active_workspace = next(iter(organization_workspaces), None)
if not active_workspace:
return {}

organization_owner = await db_manager.get_organization_owner(
organization_id=organization_id
)
project_invitations = await db_manager.get_project_invitations(
project_id=request.state.project_id

default_project = await db_manager.get_default_project_by_organization_id(
organization_id=organization_id
)
project_invitations = (
await db_manager.get_project_invitations(project_id=str(default_project.id))
if default_project
else []
)

organization_db = await db_manager.get_organization_by_id(
organization_id=organization_id
)

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 | 🟠 Major | ⚡ Quick win

Scope org-details reads to the caller's memberships.

fetch_organization_details() loads workspaces, owner metadata, and invitations for whatever organization_id is in the path, but it never verifies that request.state.user_id belongs to that organization. In OSS multi-org mode this lets any authenticated user read another organization's owner and pending-invite data if they know the UUID.

Comment on lines +367 to +380
owner_membership = next(
(membership for membership in memberships if membership.role == "owner"),
None,
)
if owner_membership is not None:
return str(owner_membership.workspace_id)

return str(workspaces[0].id)
memberships.sort(
key=lambda membership: (
membership.created_at or datetime.min.replace(tzinfo=timezone.utc),
str(membership.workspace_id),
)
)
return str(memberships[0].workspace_id)

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 | 🟠 Major | ⚡ Quick win

Make owner selection deterministic.

memberships is unordered here, so next(... role == "owner") returns whichever owner row the database happens to emit first. Once a user owns multiple workspaces, their default workspace can flip between requests even though the fallback path is explicitly sorted. Sort owner memberships with the same key before picking one, or push the owner-first ordering into SQL and select a single row.

Comment on lines +230 to +238
items.push({
key: "create-organization",
label: (
<div className="flex items-center gap-2">
<span className="text-gray-900">+ New organization</span>
</div>
),
})
items.push({type: "divider", key: "organizations-actions-divider"})

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 | 🟡 Minor | ⚡ Quick win

Honor organizationSelectionEnabled for the create action too.

The prop contract says org items should stay visible but non-actionable when selection is disabled, with logout as the only actionable escape hatch. create-organization remains enabled here, so pages that intentionally freeze org interactions can still mutate org state.

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-production-6bf6.up.railway.app/w
Image tag pr-4675-3d11670
Status Failed
Railway logs Open logs
Logs View workflow run
Updated at 2026-06-14T15:10:15.389Z

Default workspace resolution becomes membership-based in both editions
(get_default_workspace_id moves OSS-ward; the OSS oldest-workspace fallback
and get_oss_organization are gone). Organization and workspace listings are
scoped to the requesting user's memberships. The admin singleton org/workspace
delete guards and the admin_create_organization ON CONFLICT branch are
removed. Web: org switcher, New Organization, and owner submenu are enabled
in OSS.
…d-trip

The workspace-resolution unit tests move to the OSS suite (the function moved
OSS-ward; patching db_manager_ee's engine no longer reaches it). The admin
org create/delete round-trip returns to the OSS acceptance suite now that the
singleton slug-collapse and delete guard are gone.
OSS no longer needs the owner-signup + invite + re-login dance: any user can
sign up directly and gets their own organization, same as EE. global-setup
drops the license fork and inviteOssUser; AGENTA_TEST_OSS_OWNER_EMAIL stays
as an optional fixed-account login for persistent CI deployments.
The api-key auth path sets request.state.user_id to the user's DB id
(api_key.created_by_id), but get_user only matched UserDB.id when is_ee().
In OSS the new user-facing POST /organizations/ resolved the api-key user
via get_user and 404'd. Apply the id fallback in both editions, guarded so
a non-UUID SuperTokens uid still matches on uid only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend enhancement New feature or request Frontend size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant