feat(platform): prepare organization collaboration for internal rollout - #14433
feat(platform): prepare organization collaboration for internal rollout#14433ntindle wants to merge 72 commits into
Conversation
…e-team requirement Mutating team routes (settings update, member add/role-change/remove) were gated by requires_team_permission, which required the caller's active team context (X-Team-Id) to match the target team. This made header-dependent management untenable now that teams are badges/filters rather than active contexts: a team admin could only manage the team currently selected in their UI, and an org admin could delete but not rename or manage members of a team. These routes now use Security(get_request_context) and a local _authorize_team_management helper that authorizes against the target ws_id from the URL path, independent of ctx.team_id: allowed if the caller is an admin of the target team (new team_db.is_team_admin lookup) or holds org-level MANAGE_WORKSPACES. The target team must still belong to the caller's org. requires_team_permission in autogpt_libs is left untouched. Adds scenario-named route tests covering team-admin (no active-team header), org-admin non-member, plain org member, cross-team admin, and cross-org cases for both settings and member-management routes; updates the now-obsolete active-team-mismatch regression test to the new semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm3mCG9okfdGtAXtFaDF9A
Mutating team routes (update/delete/join/leave team, add/remove member) call team_db functions that raise ValueError for user-triggerable rejections — changing the default team's join policy, deleting or leaving the default team, removing the last admin, adding a non-org-member, and self-joining a non-OPEN team. The handlers didn't catch them, so at the router level these surface as 500s instead of 400s. Add a small local _rejects_as_400 decorator and apply it only to the six mutating handlers whose db calls raise ValueError, so a genuine bug elsewhere still surfaces as a 500 rather than being masked as a 400. NotFoundError/NotAuthorizedError subclass ValueError but carry their own 404/403 mappings, so they are re-raised untouched. Adds scenario-named route tests asserting each rejection returns 400 with the db-layer message as the detail (not a generic 500). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm3mCG9okfdGtAXtFaDF9A
… is_member
Org owners/admins (MANAGE_WORKSPACES) now see PRIVATE teams they don't belong
to in team lists as name + member count only — the description is redacted
("governance without surveillance"). Regular members still don't see them at
all. Every returned team row carries a per-caller is_member flag so the
frontend can render Join/Manage affordances correctly.
- TeamResponse gains is_member; from_db can redact the description.
- list_teams takes can_manage_workspaces: admins bypass the OPEN/member
visibility filter and get redacted rows for PRIVATE non-member teams.
is_member and active member_count are computed in bulk (two queries, no
N+1) via _member_facts.
- New get_team_for_viewer backs GET /{ws_id}: OPEN/member teams full, admin
non-member PRIVATE teams redacted, regular-member PRIVATE non-member 404 —
so details mirrors list visibility instead of exposing every team by id.
- Regenerated the frontend OpenAPI schema.
Adds scenario-named route tests for admin/member list visibility, redaction,
is_member accuracy on open/private teams, and details-route redaction/404.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm3mCG9okfdGtAXtFaDF9A
The members route was open to any org member, letting anyone enumerate a private workspace's roster that the list/details routes hide. The roster is contents: members and OPEN workspaces list normally; org admins get a 403 (join to view — governance without surveillance); regular non-members get the same 404 as the rest of the visibility surface. Surfaced by the expandable-members product ask, which lazily fetches rosters from the teams list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm3mCG9okfdGtAXtFaDF9A
…enial branches Address review on #13524: - Reformat routes_test.py (black: missing blank line between classes, introduced by the dev merge) — unblocks the red lint CI job - Add route-level allow + deny tests for POST /{ws_id}/members and DELETE /{ws_id}/members/{uid} so dropping _authorize_team_management from either route fails a test - Cover is_team_admin denial branches: active non-admin member, and admin rows with INVITED/SUSPENDED status - Cover the ctx.org_id != org_id guard (403 before any DB lookup) - Document the deliberate delete-vs-manage asymmetry on delete_team Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The /credits balance, transaction-history, invoice, top-up, refund, auto-top-up and payment-method routes in api/features/v1.py resolve their credit model through the request's org context. For a pooled (real) org this routes to the shared OrgBalance, so any active org member — regardless of role — could read the org's balance and full transaction/invoice history and initiate billing mutations, since the routes were gated only by requires_user. Swap those routes to require org-level MANAGE_BILLING (owner or billing_manager) via requires_org_permission, matching orgs/routes.py. Personal-org owners always hold is_org_owner, so the gate is a no-op for them. User-scoped subscription-tier routes and the unauthenticated Stripe webhook are intentionally left unchanged. Response shapes are unchanged; data-layer credit logic is untouched (HTTP layer only). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm3mCG9okfdGtAXtFaDF9A
Add GET /api/orgs/{org_id}/spend, an org billing report that aggregates the
org's USAGE (debit) ledger grouped by the team each debit was attributed to.
Gated by org-level MANAGE_BILLING (owner or billing_manager) via
requires_org_permission, matching the SECRT-2449 credits-route gating.
- data/org_credit.py: get_org_spend_by_team() groups OrgCreditTransaction by
teamId (Prisma group_by), filters to type=USAGE + isActive, negates the debit
sum into a positive total_spent, resolves team names in a second Team query,
and preserves NULL-team usage (org-home / legacy migrations) as an
unattributed bucket. Buckets are returned highest-spend first.
- orgs/model.py: OrgSpendResponse / TeamSpendBucket response models (snake_case).
- orgs/routes.py: the org-scoped route with an optional from/to datetime window.
Team-scoped VIEW_SPEND routes are intentionally deferred to the team-facing UI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm3mCG9okfdGtAXtFaDF9A
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm3mCG9okfdGtAXtFaDF9A
Add POST /api/orgs/{org_id}/avatar (multipart) gated by
requires_org_permission(RENAME_ORG) + _verify_org_path, mirroring the
store submission media upload: images-only content-type + extension
allowlist at the route, then magic-byte checks, 50MB cap, ClamAV scan,
and GCS upload via store_media.upload_media. upload_media gains an
organization_id kwarg that scopes the storage path to
orgs/{org_id}/images/{uuid}{ext} (server-side ids, never client-named).
The URL is persisted via org_db.update_org and the updated OrgResponse
(which already carries avatar_url) is returned.
Co-Authored-By: Claude Opus <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBwmh7CkHiF8vKLBf2GWTU
# Conflicts:
# autogpt_platform/backend/backend/api/features/orgs/routes.py
…e-labeled recall, governed shared writes Co-Authored-By: Claude Opus <noreply@anthropic.com>
…ate tiers, reject bad tiers - org memory read (resolve_warm_targets/resolve_search_targets) and write (memory_store) now re-verify ACTIVE org membership via is_org_member instead of trusting session.organization_id (only checked at session creation, so a revoked/stale membership previously reached org memory). Mirrors the team tier. - resolve_warm_targets isolates a malformed org id: derivation failure skips the org tier instead of sinking the personal warm context. - resolve_search_targets rejects unknown tiers with TierError instead of silently returning no targets (which read as "no memories"). Co-Authored-By: Claude Opus <noreply@anthropic.com>
…ure tripwire) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…+ held-memory review (SECRT-2455) Adds the three org-admin endpoints the shared-memory UI (frontend #13658) flagged as missing: 1. holdBuffer toggle persistence: memory_hold_buffer on UpdateOrgRequest (None = leave unchanged) and OrgResponse (default true), read/written at Organization.settings["memory"]["holdBuffer"] — the exact key copilot/graphiti/tiers.hold_buffer_enabled reads. Read-modify-write preserves sibling settings keys; gated by RENAME_ORG (same as the org profile PATCH it rides on). 2. GET /api/orgs/{org_id}/memory/held — lists tentative ("held") memories across the org tier and every team tier of the org, tier-labelled, excluding personal + other orgs' teams. MANAGE_MEMBERS gated. 3. POST .../held/{memory_id}/{approve,reject} — approve reuses the dream ratification status-flip (_promote_if_tentative), reject reuses memory_forget's soft-retract (mark_edges_superseded). Both validate the edge lives in one of THIS org's shared tiers before touching it (404 otherwise); personal tiers are never touchable. Tests mock at the graphiti-driver + prisma boundary. Co-Authored-By: Claude Opus <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBwmh7CkHiF8vKLBf2GWTU
…am_id on list responses Co-Authored-By: Claude Opus <noreply@anthropic.com>
…y_agent - fork_library_agent now passes organization_id/team_id to fork_graph, so the forked AgentGraph rows are tenanted, not just the library entry. Extended the tenancy test to assert fork_graph receives the org/team. - hoist v1_test.py helper imports to module scope per the backend top-level-imports guideline. Co-Authored-By: Claude Opus <noreply@anthropic.com>
The frontend retired the active-team header (X-Team-Id is always null),
so builder save, schedule create and API key create silently stamped
every new resource org-home. Add an explicit, membership-validated
team_id to those create/save surfaces:
- POST /graphs and PUT /graphs/{id}: team_id in the CreateGraph body /
query param. On a new version, inherit the agent's existing team when
omitted so re-saving no longer moves a team agent back to org-home.
- POST /graphs/{id}/schedules: team_id in ScheduleCreationRequest,
inheriting the scheduled agent's team when omitted.
- POST /api-keys: team_id in CreateAPIKeyRequest, mapped to
teamIdRestriction (the enforced team scope for a key).
Creating into a team requires ACTIVE membership (org admins must join
first); an invalid or cross-org team returns 400. Regenerated openapi.json.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm3mCG9okfdGtAXtFaDF9A
…ests with tenancy team-field-alignment threads organization_id/team_id through fork_library_agent and surfaces owningOrgId as StoreSubmission.organization_id, colliding with dev's new regression assertions: - test_regression_fork_library_agent_creates_for_caller: fork_graph is now called with organization_id/team_id kwargs; with no active org/team both are None. Update the exact-call assertion accordingly (no ANY). - test_regression_create_submission_sets_owning_user: from_listing_version reads _l.owningOrgId into StoreSubmission.organization_id (str | None); the mocked listing returned a bare MagicMock and failed Pydantic validation. Set owningOrgId = None on the fixture. notifications_test.py::test_upsert_creates_empty_batch_then_appends left as-is: it is a real-DB (SpinTestServer) test that this branch does not touch (notifications source, test, and schema notification models are byte-identical to dev), passing standalone — its rollup failure is a pre-existing dev flake, not batch-branch drift. Co-Authored-By: Claude Opus <noreply@anthropic.com>
…ire forbids stdlib dataclasses in backend Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-ups on the team tenancy work: - Regenerate openapi.json so the new response-model tenancy fields (organization_id/team_id on LibraryAgent, LibraryFolder, LibraryFolderTree, SessionSummaryResponse; organization_id on StoreSubmission) are actually in the exported schema — fixes the red `check API types` job. - create_api_key: only `None` falls back to the request-context team, so an explicit empty-string team_id is rejected instead of silently widening the key's scope. - update_graph: re-tag the linked LibraryAgent row with the version's org/team, so list badges and team filters can't disagree with where the version was saved. - update_graph / schedule create: only inherit the existing team when the source row lives in the org the request is acting in, so a team from another org is never stamped onto this org's row. - Collapse the duplicate `_api_key_info` test helper (the second definition shadowed the first) and drop its redundant inline imports. - Add the missing coverage: schedule-create team resolution (explicit / inherit / reject), update_graph non-member team rejection, the no-org-context 400 branch of `_resolve_write_team_id`, the empty-string api-key case, and StoreSubmission.from_listing_version surfacing a non-null owning org. - Document why update_graph takes team_id as a query param, why api-key create re-validates ctx.team_id, and align the field descriptions with the actual request-context fallback order. - Warn (instead of debug) when an X-Team-Id header resolves to nothing, so a mistyped/expired team doesn't silently broaden what gets created. - Share one loop-closed retry helper between conftest and the search integration test, with a TODO for the teardown fix that removes it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Grant table with a polymorphic principal (TEAM enforced, USER reserved, room for PERSONA), version-pinned by default with opt-in followLatest, per-grant capability (VIEW/EXECUTE) and credential mode. Enforcement: grant fallback in get_graph and an EXECUTE-grant path in validate_graph_execution_permissions; non-TEAM principals raise loudly rather than silently half-supporting an unshipped principal type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm3mCG9okfdGtAXtFaDF9A
…ts, 403 authz - resolve_graph_grant + list_received_grants now exclude archived workspaces, matching upsert_grant's create-time rule and list_teams visibility - grant resolution/execution in graph.py constrained to the grant's organizationId so a followLatest grant can't follow a graph moved to another org - owner/admin authorization failures raise NotAuthorizedError (mapped to 403) instead of a plain ValueError (400) Co-Authored-By: Claude Opus <noreply@anthropic.com>
…s tests Dev's new get_graph/validate tests mock the AgentGraph/StoreListingVersion/ LibraryAgent prisma clients but not backend.data.grants.prisma, so the team-grant fallback in get_graph (and the exec-grant check in validate_graph_execution_permissions) hit the real, unconnected client and raised RuntimeError. - graph_test.py: add a file-wide autouse fixture patching backend.data.grants.prisma with agentgraphgrant.find_many -> [] so resolve_graph_grant returns None (no-grant path is explicit). No assertions weakened. - orgs/regression_test.py: patch backend.data.grants.prisma inline in the one non-owner get_graph test (test_regression_get_graph_wrong_org_returns_none), matching the file's per-test patch style. Co-Authored-By: Claude Opus <noreply@anthropic.com>
…d/add collision with the memory-governance mount in the batch rollup Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…er's credentials (SECRT-2448) When a user reaches a graph only via an OWNER-mode team grant, the run now resolves the graph's own stored credential references against the GRAPH OWNER's credential store (never the consumer's), injected at execution only. CONSUMER-mode grants, owners running their own graphs, and marketplace/library runs are unaffected. OWNER resolution is scoped to ids the graph itself references (allowlist), fails closed on a missing owner credential (no fallback to consumer creds), stays inert for sub-graphs, and re-checks live team membership at execution-start. upsert_grant now rejects OWNER grants created by anyone but the graph owner (an org admin can't expose a third party's creds). Co-Authored-By: Claude Opus <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBwmh7CkHiF8vKLBf2GWTU
…-bypass tests The resume-backfill test exercised the real grants resolver against live prisma — passing locally on a warm pool, failing in CI with loop-closed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…CRT-2475) Pending (including expired) invitations get a rotated token and a new 7-day expiry; the previously emailed link stops working on resend. Accepted or revoked invitations are rejected with 400. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm3mCG9okfdGtAXtFaDF9A
…th org-UI stack routes_test.py is heavily modified by the in-flight org-UI stack; appending there made the rollup eject this PR on a test-file conflict. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move get_request_context and org_router from the local _client import to top-level imports, per the backend top-level-imports guideline. Co-Authored-By: Claude Opus <noreply@anthropic.com>
…urate Addresses review feedback on the resend endpoint: - Regenerate openapi.json for the new /resend route (fixes red `check API types` CI) plus the new list query param. - Add `include_expired` to `list_invitations`. The endpoint filtered `expiresAt > now`, so an admin could never obtain the invitation_id of an expired invite -- the resend feature's primary use case was unreachable end-to-end. Defaults to false, so existing clients are unchanged. - Close the TOCTOU window between the state read and the write. `update()` only accepts a unique WHERE in prisma-client-python, so the rotation now goes through `update_many()` with `acceptedAt`/`revokedAt` re-asserted in the WHERE clause, and reads the row back by the freshly minted token. A concurrent accept/revoke now yields 400 instead of a 200 with a live token. - Re-validate teamIds on resend: teams deleted since the invite was created are pruned (and logged) instead of silently promising access that accept can no longer grant. - Extract the shared lookup+org-match guard into `_get_org_invitation`, reused by `revoke_invitation`. - Tests: assert the response body carries the rotated token (was never checked), cover `find_unique -> None`, the CAS where-clause, the lost-race path, team pruning, and both `include_expired` modes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Important Review skippedToo many files! This PR contains 943 files, which is 643 over the limit of 300. To get a review, reduce the PR to 300 files or fewer by splitting it into smaller PRs or changing its base branch. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (943)
You can disable this status message by setting the |
|
This PR is too large for Bugbot to review. It changes 58,996 lines and 3,855,755 characters. Split the change into smaller pull requests to get a review. |
|
🚧 Skipped: PR exceeds review size limit. Please split into smaller PRs and re-run. |
|
/review |
|
I couldn't load the latest pull request details from GitHub, so the review was not queued: Failed to load pull request diff: Client error '406 Not Acceptable' for url 'https://api.github.com/repos/Significant-Gravitas/AutoGPT/pulls/14433' |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## dev #14433 +/- ##
==========================================
+ Coverage 81.34% 81.37% +0.02%
==========================================
Files 3515 3663 +148
Lines 263403 281478 +18075
Branches 24413 26208 +1795
==========================================
+ Hits 214278 229042 +14764
- Misses 43780 46576 +2796
- Partials 5345 5860 +515
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
This pull request has conflicts with the base branch, please resolve those so we can evaluate the pull request. |
Why / What / How
Enable organization and team collaboration for an internal cohort, with live membership checks, scoped resources, and current Home, Files, expert, Copilot, MCP and execution behavior. Collaboration entry is gated by
SHOW_ORG_SETTINGSin both the API and frontend. The integration closes the reproduced authorization and upgrade gaps.The original tail had fallen 94 commits behind the initial dev baseline. This consolidated candidate retains the original stack history and leaves the 23 existing PRs untouched. Final integration baseline: dev
6dc5fec8b6fb49d85a259b037c489eb187d96f34, org tail #13661e9043a40271ca270a4d4784fc6867790a470dfe0. It includes the later dev hotfix that makes marketplace expert profiles public and replaces hiring controls with Coming soon, plus the updated security policy. The repository uses squash merges; disposition of the original PRs is a separate step after this candidate is reviewed.Changes 🏗️
Candidate commit:
3238118977c0ad3e116f47a3b0c021427e2c1512. Its Git tree8b3951c47e47bf6b061e04500b3813a6a644f979exactly matches the final locally tested image (sha256:87324eeb99bec3653095c754844b2435ee31929203afacc40846a149df6a898c). Hosted checks and fresh review approval must be verified on this PR before merging.docs/platform/organizations/internal-rollout.md; access limits are inaccess-model.md.Credential records remain personally owned; explicitly consented OWNER-mode grants authorize use only within their exact organization/team. Experts remain private to their personal-workspace owner, and Files is the owner's workspace filtered by organization/team. Shared team drives and shared experts are outside this initial internal rollout. Quarantined historical files require operator/owner review before an affected account joins the cohort.
Agents and large language models used
Codex with GPT-6 and delegated Codex agents.
Checklist 📋
For code changes:
Old stack CI and failed pre-fix/local-fixture runs remain preserved as historical evidence. The local verification here is not a claim that old member heads or hosted production were tested.
Focused regression evidence includes 134 grant/executor tests, 8 real database OWNER-grant cases plus an independent 3-case persisted-scope/secret-access probe, 9 real database shared-agent copy cases, 12 real database credit-history cases, 19 billing-link frontend tests, and 47 avatar/settings tests. These suites overlap with broader runs and are not an additive total. The internal browser rollout used real Better Auth sessions and backend JWT verification for synthetic owner, member and outsider accounts in an isolated local database. Complete-candidate file checks, Gitleaks and detect-secrets passed; exactly two non-secret UI/test literals were reviewed and recorded without changing detector settings.
For configuration changes:
.env.defaultdocuments the local org overridedocker-compose.ymlremains compatible with existing dependenciesHosted rollout must target the same account IDs for
SHOW_ORG_SETTINGSin backend and frontend; leave global force overrides unset. Disabling the flag stops new collaboration entry, while existing authorized reads and cleanup continue. Deploy all tenant-aware workers together and do not revert to old unscoped services over new tenant data.Follow-ups retained: measure webhook enqueue lock contention before changing its authorization pin; optional chat-title rename retry must obtain fresh authorization after a lease-release failure.