Skip to content

feat(backend): accept explicit team_id on create/save flows - #13540

Open
ntindle wants to merge 2 commits into
devfrom
ntindle/secrt-2462-team-picker-on-createsave-flows-builder-save-agent-create
Open

feat(backend): accept explicit team_id on create/save flows#13540
ntindle wants to merge 2 commits into
devfrom
ntindle/secrt-2462-team-picker-on-createsave-flows-builder-save-agent-create

Conversation

@ntindle

@ntindle ntindle commented Jul 11, 2026

Copy link
Copy Markdown
Member

Why / What / How

Why: The badge/filter model retired the active-team header, so every created resource now lands org-home — the builder gives no way to choose a team (observed on the rollup preview, SECRT-2462). Worse, a latent bug: teamId is stored per graph version, so re-saving a team-owned agent with no team context silently moved it back to org-home.

What: Create/save endpoints accept an explicit team_id, validated as "caller is an ACTIVE member of a team in their org" (org admins are not exempt — creating into a team requires membership; join first):

  • POST /graphsteam_id on CreateGraph
  • PUT /graphs/{id}team_id query param; when omitted, the graph inherits its existing tenant instead of resetting to org-home (the re-save bug fix, with regression test)
  • POST /graphs/{id}/schedulesteam_id on the request; omitted → inherits the graph's tenant
  • POST /api-keysteam_idteamIdRestriction (the enforced team dimension for keys; the ownership teamId column is dead for user-owned keys)

How: One shared _resolve_write_team_id helper over the existing get_user_team_ids (membership implies org-belonging in a single check; None → org-home; invalid/non-member/cross-org → 400). Explicit value wins over the (now always-null, back-compat) header context. Data layer unchanged — it already took the ids as params; only route stamping changed. OpenAPI regenerated.

Frontend picker (shared component + builder save integration) is the follow-up slice on the org-UI chain.

Linear: SECRT-2462 (backend half)

Changes 🏗️

  • backend/api/model.py, backend/api/features/v1.py: team_id on the four surfaces + _resolve_write_team_id
  • backend/api/features/v1_test.py: +9 scenario tests (explicit stamp, omitted→NULL, omitted-on-update→inherit, non-member 400, cross-org 400, api-key restriction)
  • frontend/src/app/api/openapi.json: regenerated

Checklist 📋

For code changes:

  • I have clearly listed my changes in the PR description
  • I have made a test plan
  • I have tested my changes according to the test plan:
    • poetry run pytest backend/api/features/v1_test.py -q — 58 passed; orgs suite 233 passed
    • poetry run format clean

For changes touching data/*.py: n/a — data layer untouched; team validation derives strictly from the caller's own ACTIVE memberships.

For configuration changes: n/a

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jm3mCG9okfdGtAXtFaDF9A


Note

Medium Risk
Changes tenancy stamping on graph versions, schedules, and API keys; wrong resolution could misplace resources, but membership validation and regression tests reduce that risk.

Overview
Adds explicit team_id on graph create, graph save, schedule create, and API key create so tenancy no longer depends on the retired X-Team-Id header. A shared _resolve_write_team_id helper checks org context and active team membership (org admins must still be members); invalid or cross-org teams return 400.

PUT /graphs/{id} now inherits the agent’s existing team when team_id is omitted, fixing re-saves that silently moved team-owned agents to org-home. Create and schedule flows use explicit team_id when provided, else legacy header context, else org-home or the graph’s team. API keys map optional team_id to teamIdRestriction. OpenAPI and nine route tests cover stamping, inheritance, and rejection paths.

Reviewed by Cursor Bugbot for commit 737cdc3. Bugbot is set up for automated code reviews on this repo. Configure here.

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
@ntindle
ntindle requested a review from a team as a code owner July 11, 2026 04:41
@ntindle
ntindle requested review from Pwuts and Swiftyos and removed request for a team July 11, 2026 04:41
@github-project-automation github-project-automation Bot moved this to 🆕 Needs initial review in AutoGPT development kanban Jul 11, 2026
@github-actions github-actions Bot added platform/frontend AutoGPT Platform - Front end platform/backend AutoGPT Platform - Back end labels Jul 11, 2026
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Optional team_id inputs now support validated team-scoped graph creation, graph version updates, schedules, and API keys. Resolved team identifiers are persisted or inherited, user creation exposes status through a response header, and graph migration responses report skipped webhook presets.

Changes

Team-scoped writes

Layer / File(s) Summary
Team resolution and graph creation
autogpt_platform/backend/backend/api/features/v1.py, autogpt_platform/backend/backend/api/model.py, autogpt_platform/backend/backend/api/features/v1_test.py, autogpt_platform/frontend/src/app/api/openapi.json
A shared resolver validates active membership, and graph creation persists explicit or context-derived team identifiers on graphs and library agents.
Version and schedule team binding
autogpt_platform/backend/backend/api/features/v1.py, autogpt_platform/backend/backend/api/features/v1_test.py, autogpt_platform/frontend/src/app/api/openapi.json
Graph updates accept optional team parameters with inheritance behavior, while schedules resolve explicit, contextual, or graph-inherited team identifiers.
API key team restriction
autogpt_platform/backend/backend/api/features/v1.py, autogpt_platform/backend/backend/api/model.py, autogpt_platform/backend/backend/api/features/v1_test.py, autogpt_platform/frontend/src/app/api/openapi.json
API key creation accepts an optional team restriction, validates membership, preserves unrestricted keys when omitted, and rejects unauthorized teams.
User and graph response reporting
autogpt_platform/backend/backend/api/features/v1.py, autogpt_platform/backend/backend/api/features/v1_test.py
User creation sets X-AutoGPT-User-Created; graph update and activation responses include skipped webhook presets from migration.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant API
  participant MembershipValidation
  participant GraphOrScheduler
  Client->>API: submit write with optional team_id
  API->>MembershipValidation: validate active membership
  MembershipValidation-->>API: resolved team scope
  API->>GraphOrScheduler: persist or schedule with resolved scope
  GraphOrScheduler-->>Client: response with operation status
Loading

Possibly related PRs

Suggested reviewers: swiftyos, pwuts

Poem

A rabbit hops through teams so neat,
Stamping graphs with scopes complete.
Keys and schedules join the parade,
Webhook skips are now displayed.
“Hop-hop!” says Bun, “the work is made!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly and concisely summarizes the main change: accepting explicit team_id in backend create and save flows.
Description check ✅ Passed The description is directly related to the changeset and clearly explains team validation, inheritance behavior, tests, and OpenAPI updates.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ntindle/secrt-2462-team-picker-on-createsave-flows-builder-save-agent-create

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.

@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR Overlap Detection

This check compares your PR against all other open PRs targeting the same branch to detect potential merge conflicts early.

🟢 Low Risk — File Overlap Only

These PRs touch the same files but different sections (click to expand)

Summary: 0 conflict(s), 0 medium risk, 4 low risk (out of 4 PRs with file overlap)


Auto-generated on push. Ignores: openapi.json, lock files.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@autogpt_platform/backend/backend/api/features/v1_test.py`:
- Around line 1020-1280: Add coverage for schedule creation in the existing
team-resolution tests: add tests for the POST /graphs/{id}/schedules flow
verifying that a membership-validated explicit team_id is forwarded to
add_execution_schedule, an omitted team_id inherits graph.team_id, and an
invalid team_id returns 400 without invoking the scheduler. Reuse the existing
get_user_team_ids mocking and schedule request fixtures/helpers, targeting the
schedule creation handler and add_execution_schedule mock.
🪄 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: CHILL

Plan: Pro

Run ID: cd26a34e-4764-4dc6-9aa8-346281565ce6

📥 Commits

Reviewing files that changed from the base of the PR and between 70928bb and 654c5c8.

📒 Files selected for processing (4)
  • autogpt_platform/backend/backend/api/features/v1.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/model.py
  • autogpt_platform/frontend/src/app/api/openapi.json
📜 Review details
⏰ Context from checks skipped due to timeout. (11)
  • GitHub Check: check API types
  • GitHub Check: lint
  • GitHub Check: integration_test
  • GitHub Check: end-to-end tests
  • GitHub Check: type-check (3.13)
  • GitHub Check: Check PR Status
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.12)
  • GitHub Check: type-check (3.11)
  • GitHub Check: test (3.11)
  • GitHub Check: type-check (3.12)
🧰 Additional context used
📓 Path-based instructions (5)
autogpt_platform/backend/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

autogpt_platform/backend/**/*.py: Use Python 3.11 (required; managed by Poetry via pyproject.toml) for backend development
Always run 'poetry run format' (Black + isort) before linting in backend development
Always run 'poetry run lint' (ruff) after formatting in backend development

autogpt_platform/backend/**/*.py: Use poetry run ... command for executing Python package dependencies
Use top-level imports only — avoid local/inner imports except for lazy imports of heavy optional dependencies like openpyxl
Use absolute imports with from backend.module import ... for cross-package imports; single-dot relative imports are acceptable for sibling modules within the same package; avoid double-dot relative imports
Do not use duck typing — avoid hasattr/getattr/isinstance for type dispatch; use typed interfaces/unions/protocols instead
Use Pydantic models over dataclass/namedtuple/dict for structured data
Do not use linter suppressors — no # type: ignore, # noqa, # pyright: ignore; fix the type/code instead
Prefer list comprehensions over manual loop-and-append patterns
Use early return with guard clauses first to avoid deep nesting
Use %s for deferred interpolation in debug log statements for efficiency; use f-strings elsewhere for readability (e.g., logger.debug("Processing %s items", count) vs logger.info(f"Processing {count} items"))
Sanitize error paths by using os.path.basename() in error messages to avoid leaking directory structure
Be aware of TOCTOU (Time-Of-Check-Time-Of-Use) issues — avoid check-then-act patterns for file access and credit charging
Use transaction=True for Redis pipelines to ensure atomicity on multi-step operations
Use max(0, value) guards for computed values that should never be negative
Keep files under ~300 lines; if a file grows beyond this, split by responsibility (extract helpers, models, or a sub-module into a new file)
Keep functions under ~40 lines; extract named helpers when a function grows longer
...

Files:

  • autogpt_platform/backend/backend/api/model.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
autogpt_platform/{backend,autogpt_libs}/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Format Python code with poetry run format

Files:

  • autogpt_platform/backend/backend/api/model.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
autogpt_platform/backend/**/api/**/*.py

📄 CodeRabbit inference engine (autogpt_platform/backend/AGENTS.md)

autogpt_platform/backend/**/api/**/*.py: Use Security() instead of Depends() for authentication dependencies to get proper OpenAPI security specification
Follow SSE (Server-Sent Events) protocol: use data: lines for frontend-parsed events (must match Zod schema) and : comment lines for heartbeats/status

Files:

  • autogpt_platform/backend/backend/api/model.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
autogpt_platform/backend/backend/api/features/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Update routes in '/backend/backend/api/features/' and add/update Pydantic models in the same directory for API development

Files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
autogpt_platform/backend/**/*_test.py

📄 CodeRabbit inference engine (autogpt_platform/backend/AGENTS.md)

autogpt_platform/backend/**/*_test.py: Use pytest with snapshot testing for API responses
Colocate test files with source files using *_test.py naming convention
Mock at boundaries — mock where the symbol is used, not where it's defined; after refactoring, update mock targets to match new module paths
Use AsyncMock from unittest.mock for async functions in tests
When writing tests, use Test-Driven Development (TDD): write failing tests marked with @pytest.mark.xfail before implementation, then remove the marker once the implementation is complete
When creating snapshots in tests, use poetry run pytest path/to/test.py --snapshot-update; always review snapshot changes with git diff before committing

Files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
🧠 Learnings (14)
📚 Learning: 2026-02-26T17:02:22.448Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12211
File: .pre-commit-config.yaml:160-179
Timestamp: 2026-02-26T17:02:22.448Z
Learning: Keep the pre-commit hook pattern broad for autogpt_platform/backend to ensure OpenAPI schema changes are captured. Do not narrow to backend/api/ alone, since the generated schema depends on Pydantic models across multiple directories (backend/data/, backend/blocks/, backend/copilot/, backend/integrations/, backend/util/). Narrowing could miss schema changes and cause frontend type desynchronization.

Applied to files:

  • autogpt_platform/backend/backend/api/model.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-03-05T15:42:08.207Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12297
File: .claude/skills/backend-check/SKILL.md:14-16
Timestamp: 2026-03-05T15:42:08.207Z
Learning: In Python files under autogpt_platform/backend (recursively), rely on poetry run format to perform formatting (Black + isort) and linting (ruff). Do not run poetry run lint as a separate step after poetry run format, since format already includes linting checks.

Applied to files:

  • autogpt_platform/backend/backend/api/model.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-03-16T16:35:40.236Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12440
File: autogpt_platform/backend/backend/api/features/workflow_import.py:54-63
Timestamp: 2026-03-16T16:35:40.236Z
Learning: Avoid using the word 'competitor' in public-facing identifiers and text. Use neutral naming for API paths, model names, function names, and UI text. Examples: rename 'CompetitorFormat' to 'SourcePlatform', 'convert_competitor_workflow' to 'convert_workflow', '/competitor-workflow' to '/workflow'. Apply this guideline to files under autogpt_platform/backend and autogpt_platform/frontend.

Applied to files:

  • autogpt_platform/backend/backend/api/model.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-03-31T15:37:38.626Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12623
File: autogpt_platform/backend/backend/copilot/tools/agent_generator/fixer.py:37-47
Timestamp: 2026-03-31T15:37:38.626Z
Learning: When validating/constructing Anthropic API model IDs in Significant-Gravitas/AutoGPT, allow the hyphen-separated Claude Opus 4.6 model ID `claude-opus-4-6` (it corresponds to `LlmModel.CLAUDE_4_6_OPUS` in `autogpt_platform/backend/backend/blocks/llm.py`). Do NOT require the dot-separated form in Anthropic contexts. Only OpenRouter routing variants should use the dot separator (e.g., `anthropic/claude-opus-4.6`); `claude-opus-4-6` should be treated as correct when passed to Anthropic, and flagged only if it’s used in the OpenRouter path where the dot form is expected.

Applied to files:

  • autogpt_platform/backend/backend/api/model.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-04-15T02:43:36.890Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12780
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:0-0
Timestamp: 2026-04-15T02:43:36.890Z
Learning: When reviewing Python exception handlers, do not flag `isinstance(e, X)` checks as dead/unreachable if the caught exception `X` is a subclass of the exception type being handled. For example, if `X` (e.g., `VirusScanError`) inherits from `ValueError` (directly or via an intermediate class) and it can be raised within an `except ValueError:` block, then `isinstance(e, X)` inside that handler is reachable and should not be treated as dead code.

Applied to files:

  • autogpt_platform/backend/backend/api/model.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-05-23T05:29:43.085Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 13200
File: autogpt_platform/backend/backend/executor/scheduler.py:590-593
Timestamp: 2026-05-23T05:29:43.085Z
Learning: When reviewing Python code that uses Pydantic discriminated/tagged unions (e.g., `Annotated[Union[...], Field(discriminator="kind")]`), recognize that using `isinstance(x, SomeVariantInfo)` to narrow the union is an intentional and correct runtime guard and should also enable static type narrowing in tools like Pyright. Do not recommend replacing such `isinstance`-based narrowing with `cast(...)` when the check already proves the variant at runtime.

Applied to files:

  • autogpt_platform/backend/backend/api/model.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-04-22T11:46:04.431Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12881
File: autogpt_platform/backend/backend/copilot/config.py:0-0
Timestamp: 2026-04-22T11:46:04.431Z
Learning: Do not flag the Claude Sonnet 4.6 model ID as incorrect when it uses the project’s established hyphenated convention: `anthropic/claude-sonnet-4-6`. This hyphen form is the intentional, production convention and should be treated as valid (including in files like llm.py, blocks tests, reasoning.py, `_is_anthropic_model` tests, and config defaults). Note that OpenRouter also accepts the dot variant `anthropic/claude-sonnet-4.6`, so either form may be tolerated, but `anthropic/claude-sonnet-4-6` should be considered the standard to match project usage.

Applied to files:

  • autogpt_platform/backend/backend/api/model.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-04-22T11:46:12.892Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12881
File: autogpt_platform/backend/backend/copilot/baseline/service.py:322-332
Timestamp: 2026-04-22T11:46:12.892Z
Learning: In this codebase (Significant-Gravitas/AutoGPT), OpenRouter-routed Anthropic model IDs should use the hyphen-separated convention (e.g., `anthropic/claude-sonnet-4-6`, `anthropic/claude-opus-4-6`). Although OpenRouter may accept both hyphen and dot variants, treat the hyphen-separated form as the intended, correct codebase-wide convention and do not flag it as an error. Only flag the dot-separated variant (e.g., `anthropic/claude-sonnet-4.6`) as incorrect when reviewing/validating model ID strings for OpenRouter-routed Anthropic models.

Applied to files:

  • autogpt_platform/backend/backend/api/model.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-05-07T18:48:14.242Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 13040
File: autogpt_platform/backend/backend/blocks/llm.py:0-0
Timestamp: 2026-05-07T18:48:14.242Z
Learning: In this repository, isort may split imports from the same module into separate blocks when some imports are aliased (e.g., `from module import X as Y`) and others are not. Preserve the two-block layout when it results from isort (such as keeping `from openai.types.chat import ChatCompletion as OpenAIChatCompletion` separate from non-aliased imports from `openai.types.chat`). Do not treat that split as a style issue during review; merging them into a single block can fail CI with `Imports are incorrectly sorted and/or formatted`.

Applied to files:

  • autogpt_platform/backend/backend/api/model.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-05-26T14:24:34.866Z
Learnt from: Abhi1992002
Repo: Significant-Gravitas/AutoGPT PR: 13217
File: autogpt_platform/backend/backend/api/features/search/service.py:137-137
Timestamp: 2026-05-26T14:24:34.866Z
Learning: In the Significant-Gravitas/AutoGPT backend, treat `user_id` (an opaque UUID used only for correlation/tracing) as non-PII. Do not flag direct logging of `user_id` in `logger.warning`/`logger.info` statements as a PII exposure issue, as the established convention is to log `user_id` for tracing while reserving PII for fields like email or display name.

Applied to files:

  • autogpt_platform/backend/backend/api/model.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-06-11T19:39:10.493Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 13337
File: autogpt_platform/backend/backend/copilot/graphiti/reranker.py:0-0
Timestamp: 2026-06-11T19:39:10.493Z
Learning: In the Significant-Gravitas/AutoGPT Python backend, when calling the OpenAI Python client `chat.completions.create`, construct the `messages` payload using the concrete typed-dict variants from `openai.types.chat` (e.g., `ChatCompletionSystemMessageParam`, `ChatCompletionUserMessageParam`, etc.) rather than trying to instantiate `ChatCompletionMessageParam` directly. `ChatCompletionMessageParam` is a `Union` alias and is not constructible, so `ChatCompletionMessageParam(role=..., content=...)` should fail type checking. Build each message element with the appropriate concrete typed dict and then annotate the resulting list as `list[ChatCompletionMessageParam]` (e.g., `messages: list[ChatCompletionMessageParam] = [ChatCompletionSystemMessageParam(...), ...]`).

Applied to files:

  • autogpt_platform/backend/backend/api/model.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-03-01T07:58:56.207Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12213
File: autogpt_platform/frontend/src/app/api/openapi.json:10030-10037
Timestamp: 2026-03-01T07:58:56.207Z
Learning: When a backend field represents sensitive data, use a secret type (e.g., Pydantic SecretStr with length constraints) so OpenAPI marks it as a password/writeOnly field. Apply this pattern to similar sensitive request fields across API schemas so generated TypeScript clients and docs treat them as secrets and do not mishandle sensitivity. Review all openapi.jsons where sensitive inputs are defined and replace plain strings with SecretStr-like semantics with appropriate minLength constraints.

Applied to files:

  • autogpt_platform/frontend/src/app/api/openapi.json
📚 Learning: 2026-04-14T06:39:49.111Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12773
File: autogpt_platform/frontend/src/app/api/openapi.json:12803-12806
Timestamp: 2026-04-14T06:39:49.111Z
Learning: In OpenAPI specs, ensure the schema/message length caps for the StreamChatRequest.message and QueuePendingMessageRequest.message fields are set to the intended values: StreamChatRequest.message maxLength must be 64000 and QueuePendingMessageRequest.message maxLength must be 32000. Keep QueuePendingMessageRequest.message consistent with PendingMessage.content, and ensure the pending (queue) ceiling never exceeds the stream ceiling because both ultimately feed the same LLM context window. Update any legacy smaller limits (e.g., 4000/16000) to these newer ceilings.

Applied to files:

  • autogpt_platform/frontend/src/app/api/openapi.json
📚 Learning: 2026-03-07T07:43:09.871Z
Learnt from: kcze
Repo: Significant-Gravitas/AutoGPT PR: 12328
File: autogpt_platform/frontend/src/app/api/openapi.json:1116-1118
Timestamp: 2026-03-07T07:43:09.871Z
Learning: For autogpt_platform/frontend/src/app/api/openapi.json, preserve the existing behavior: HTTPBearerJWT is declared at the router level with Depends(auth.get_user_id) returning None for unauthenticated users; treat as optional auth. Do not change per-operation security descriptions unless you plan a repo-wide OpenAPI update. If you change this file, prefer clarifying operation descriptions rather than altering security requirements.

Applied to files:

  • autogpt_platform/frontend/src/app/api/openapi.json
🔇 Additional comments (4)
autogpt_platform/backend/backend/api/features/v1.py (1)

1683-1712: LGTM!

Also applies to: 1724-1751, 1791-1839, 2450-2511, 2844-2857

autogpt_platform/backend/backend/api/model.py (1)

46-52: LGTM!

Also applies to: 59-66

autogpt_platform/backend/backend/api/features/v1_test.py (1)

1020-1062: LGTM!

Also applies to: 1064-1145, 1147-1207, 1209-1280

autogpt_platform/frontend/src/app/api/openapi.json (1)

5562-5572: LGTM!

Also applies to: 15125-15129, 15168-15172, 20954-20958

Comment on lines +1020 to +1280
# ---------------------------------------------------------------------------
# Team-picker on create/save flows (SECRT-2462)
#
# The frontend retired the active-team header (X-Team-Id is now always null),
# so create/save endpoints must accept an explicit team_id in the request
# instead of silently stamping from the header. These tests lock in the
# resolution: an explicit, membership-validated team wins; an invalid team is
# rejected; and omitting it preserves each surface's safe default.
# ---------------------------------------------------------------------------
TEAM_ID = "team-abc"
_GRAPH_BODY = {"name": "Test Graph", "description": "Test", "nodes": [], "links": []}


def _saved_graph_model(user_id: str) -> GraphModel:
return GraphModel(
id="graph-123",
version=1,
is_active=True,
name="Test Graph",
description="A test graph",
user_id=user_id,
created_at=datetime(2025, 9, 4, 13, 37),
)


def _mock_graph_create_pipeline(mocker: pytest_mock.MockFixture, saved: GraphModel):
"""Stub the create pipeline so only team stamping is under test."""
mocker.patch(
"backend.api.features.v1.graph_db.make_graph_model", return_value=Mock()
)
mocker.patch(
"backend.api.features.v1.before_graph_activate",
new=AsyncMock(return_value=saved),
)
create_graph_mock = mocker.patch(
"backend.api.features.v1.graph_db.create_graph",
new=AsyncMock(return_value=saved),
)
create_lib_agent_mock = mocker.patch(
"backend.api.features.v1.library_db.create_library_agent", new=AsyncMock()
)
return create_graph_mock, create_lib_agent_mock


def test_create_new_graph_stamps_explicit_team(
mocker: pytest_mock.MockFixture, test_user_id: str
) -> None:
"""A valid team_id in the body lands the agent (and its library entry)
under that team."""
create_graph_mock, create_lib_agent_mock = _mock_graph_create_pipeline(
mocker, _saved_graph_model(test_user_id)
)
mocker.patch(
"backend.api.features.v1.get_user_team_ids",
new=AsyncMock(return_value=[TEAM_ID]),
)

response = client.post("/graphs", json={"graph": _GRAPH_BODY, "team_id": TEAM_ID})

assert response.status_code == 200
assert create_graph_mock.await_args.kwargs["team_id"] == TEAM_ID
assert create_lib_agent_mock.await_args.kwargs["team_id"] == TEAM_ID


def test_create_new_graph_omitted_team_lands_org_home(
mocker: pytest_mock.MockFixture, test_user_id: str
) -> None:
"""No team_id -> org-home (teamId NULL), and no membership lookup happens."""
create_graph_mock, create_lib_agent_mock = _mock_graph_create_pipeline(
mocker, _saved_graph_model(test_user_id)
)
get_teams = mocker.patch(
"backend.api.features.v1.get_user_team_ids",
new=AsyncMock(return_value=[TEAM_ID]),
)

response = client.post("/graphs", json={"graph": _GRAPH_BODY})

assert response.status_code == 200
assert create_graph_mock.await_args.kwargs["team_id"] is None
assert create_lib_agent_mock.await_args.kwargs["team_id"] is None
get_teams.assert_not_awaited()


def test_create_new_graph_rejects_non_member_team(
mocker: pytest_mock.MockFixture, test_user_id: str
) -> None:
"""A team the caller isn't an active member of -> 400, nothing persisted."""
create_graph_mock, _ = _mock_graph_create_pipeline(
mocker, _saved_graph_model(test_user_id)
)
mocker.patch(
"backend.api.features.v1.get_user_team_ids",
new=AsyncMock(return_value=[]),
)

response = client.post("/graphs", json={"graph": _GRAPH_BODY, "team_id": TEAM_ID})

assert response.status_code == 400
assert "active member" in response.json()["detail"]
create_graph_mock.assert_not_awaited()


def test_create_new_graph_rejects_cross_org_team(
mocker: pytest_mock.MockFixture, test_user_id: str
) -> None:
"""A team in another org never appears in the caller's team list -> 400.

get_user_team_ids only returns teams within the caller's org, so a
cross-org team is indistinguishable from a non-member team here — both
reject. We assert the org-scoped list can't be bypassed by a foreign id."""
create_graph_mock, _ = _mock_graph_create_pipeline(
mocker, _saved_graph_model(test_user_id)
)
mocker.patch(
"backend.api.features.v1.get_user_team_ids",
new=AsyncMock(return_value=["team-in-my-org"]),
)

response = client.post(
"/graphs", json={"graph": _GRAPH_BODY, "team_id": "team-in-another-org"}
)

assert response.status_code == 400
create_graph_mock.assert_not_awaited()


def _mock_update_graph_pipeline(
mocker: pytest_mock.MockFixture, existing: Mock, saved: GraphModel
):
mocker.patch(
"backend.api.features.v1.graph_db.get_graph_all_versions",
new=AsyncMock(return_value=[existing]),
)
mocker.patch(
"backend.api.features.v1.graph_db.make_graph_model",
return_value=Mock(is_active=False),
)
create_graph_mock = mocker.patch(
"backend.api.features.v1.graph_db.create_graph",
new=AsyncMock(
return_value=Mock(version=2, is_active=False, webhook_input_node=None)
),
)
mocker.patch(
"backend.api.features.v1.graph_db.get_graph",
new=AsyncMock(return_value=saved),
)
return create_graph_mock


def test_update_graph_inherits_existing_team_when_omitted(
mocker: pytest_mock.MockFixture, test_user_id: str
) -> None:
"""Regression (SECRT-2462): re-saving a team agent must keep its team, not
reset it to org-home now that the active-team header is always null."""
existing = Mock(version=1, is_active=True, team_id="team-existing")
create_graph_mock = _mock_update_graph_pipeline(
mocker, existing, _saved_graph_model(test_user_id)
)

response = client.put("/graphs/graph-123", json={"id": "graph-123", **_GRAPH_BODY})

assert response.status_code == 200
assert create_graph_mock.await_args.kwargs["team_id"] == "team-existing"


def test_update_graph_moves_to_explicit_team(
mocker: pytest_mock.MockFixture, test_user_id: str
) -> None:
"""An explicit team_id query param saves the new version into that team."""
existing = Mock(version=1, is_active=True, team_id="team-existing")
create_graph_mock = _mock_update_graph_pipeline(
mocker, existing, _saved_graph_model(test_user_id)
)
mocker.patch(
"backend.api.features.v1.get_user_team_ids",
new=AsyncMock(return_value=["team-new"]),
)

response = client.put(
"/graphs/graph-123?team_id=team-new",
json={"id": "graph-123", **_GRAPH_BODY},
)

assert response.status_code == 200
assert create_graph_mock.await_args.kwargs["team_id"] == "team-new"


def _api_key_info(user_id: str):
from prisma.enums import APIKeyStatus

from backend.data.auth.api_key import APIKeyInfo

return APIKeyInfo(
id="key-1",
name="k",
head="agpt_xxxx",
tail="yyyy",
status=APIKeyStatus.ACTIVE,
scopes=[],
user_id=user_id,
created_at=datetime(2025, 9, 4, 13, 37),
)


def test_create_api_key_stamps_team_restriction(
mocker: pytest_mock.MockFixture, test_user_id: str
) -> None:
"""A valid team_id pins the new key to that team (teamIdRestriction)."""
create_mock = mocker.patch(
"backend.api.features.v1.api_key_db.create_api_key",
new=AsyncMock(return_value=(_api_key_info(test_user_id), "agpt_plaintext")),
)
mocker.patch(
"backend.api.features.v1.get_user_team_ids",
new=AsyncMock(return_value=[TEAM_ID]),
)

response = client.post(
"/api-keys", json={"name": "k", "permissions": [], "team_id": TEAM_ID}
)

assert response.status_code == 200
assert create_mock.await_args.kwargs["team_id_restriction"] == TEAM_ID


def test_create_api_key_omitted_team_is_unrestricted(
mocker: pytest_mock.MockFixture, test_user_id: str
) -> None:
"""No team_id -> an org-wide (unrestricted) key, today's behavior."""
create_mock = mocker.patch(
"backend.api.features.v1.api_key_db.create_api_key",
new=AsyncMock(return_value=(_api_key_info(test_user_id), "agpt_plaintext")),
)

response = client.post("/api-keys", json={"name": "k", "permissions": []})

assert response.status_code == 200
assert create_mock.await_args.kwargs["team_id_restriction"] is None


def test_create_api_key_rejects_non_member_team(
mocker: pytest_mock.MockFixture,
) -> None:
"""Scoping a key to a team the caller isn't in -> 400, no key minted."""
create_mock = mocker.patch(
"backend.api.features.v1.api_key_db.create_api_key", new=AsyncMock()
)
mocker.patch(
"backend.api.features.v1.get_user_team_ids",
new=AsyncMock(return_value=[]),
)

response = client.post(
"/api-keys", json={"name": "k", "permissions": [], "team_id": TEAM_ID}
)

assert response.status_code == 400
create_mock.assert_not_awaited()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add tests for schedule creation team resolution.

The schedule creation flow (POST /graphs/{id}/schedules) accepts team_id in ScheduleCreationRequest and resolves it via the same _resolve_write_team_id helper (v1.py lines 2483-2493), but has zero test coverage. The inheritance path (resolved_team_id = graph.team_id) is unique to schedules and differs from the graph update inheritance path. Since schedules auto-execute graphs under the resolved team, this is security-sensitive.

Suggested tests:

  1. Explicit valid team_id → forwarded to add_execution_schedule as team_id.
  2. Omitted team_id → inherits graph.team_id.
  3. Invalid team_id → returns 400, scheduler not called.

Would you like me to generate these tests?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@autogpt_platform/backend/backend/api/features/v1_test.py` around lines 1020 -
1280, Add coverage for schedule creation in the existing team-resolution tests:
add tests for the POST /graphs/{id}/schedules flow verifying that a
membership-validated explicit team_id is forwarded to add_execution_schedule, an
omitted team_id inherits graph.team_id, and an invalid team_id returns 400
without invoking the scheduler. Reuse the existing get_user_team_ids mocking and
schedule request fixtures/helpers, targeting the schedule creation handler and
add_execution_schedule mock.

@ntindle

ntindle commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

/batch

@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.26214% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.92%. Comparing base (994598b) to head (737cdc3).
⚠️ Report is 29 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev   #13540      +/-   ##
==========================================
- Coverage   76.92%   76.92%   -0.01%     
==========================================
  Files        2761     2761              
  Lines      209521   209619      +98     
  Branches    20077    20173      +96     
==========================================
+ Hits       161171   161239      +68     
+ Misses      43981    43940      -41     
- Partials     4369     4440      +71     
Flag Coverage Δ
platform-backend 83.33% <91.26%> (+<0.01%) ⬆️
platform-frontend 49.15% <ø> (-0.02%) ⬇️
platform-frontend-e2e 30.97% <ø> (-0.21%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
Platform Backend 83.33% <91.26%> (+<0.01%) ⬆️
Platform Frontend 52.70% <ø> (-0.08%) ⬇️
AutoGPT Libs ∅ <ø> (∅)
Classic AutoGPT 28.43% <ø> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ntindle ntindle added the batch PR is queued in the batch-deploy rollup (batch-bot source of truth) label Jul 11, 2026
@ntindle ntindle mentioned this pull request Jul 11, 2026
11 tasks
@ntindle

ntindle commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

🤖 Added #13540 to the batch. Current batch (6): #13539, #13533, #13532, #13530, #13526, #13540.

Deploying the combined preview (#13537); /batch-merge lands them together.

ntindle added a commit that referenced this pull request Jul 22, 2026
…ent — union: explicit body param wins, ctx.team_id fallback

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the conflicts Automatically applied to PRs with merge conflicts label Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has conflicts with the base branch, please resolve those so we can evaluate the pull request.

…cker

Resolve v1.py update_graph: keep team_id Query param + adopt dev's
UpdateGraphResponse return type (same as team-field-alignment). v1_test +
orgs routes pass against Better Auth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBwmh7CkHiF8vKLBf2GWTU
@github-actions

Copy link
Copy Markdown
Contributor

Conflicts have been resolved! 🎉 A maintainer will review the pull request shortly.

@github-actions github-actions Bot removed the conflicts Automatically applied to PRs with merge conflicts label Jul 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
autogpt_platform/backend/backend/api/features/v1.py (1)

1859-1864: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not use the legacy context team when team_id is omitted.

This branch silently changes an existing graph’s tenant whenever ctx.team_id is set, and bypasses _resolve_write_team_id. Omitted team_id must inherit the active/existing version’s team_id.

Proposed fix
 if team_id is not None:
     resolved_team_id = await _resolve_write_team_id(user_id, ctx.org_id, team_id)
-elif ctx.team_id is not None:
-    resolved_team_id = ctx.team_id
 else:
     resolved_team_id = (current_active_version or existing_versions[0]).team_id
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@autogpt_platform/backend/backend/api/features/v1.py` around lines 1859 -
1864, Update the team ID selection logic around _resolve_write_team_id so an
omitted team_id never falls back to ctx.team_id. When team_id is absent, always
inherit the team_id from current_active_version or existing_versions[0],
preserving explicit team_id resolution through _resolve_write_team_id.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@autogpt_platform/backend/backend/api/features/v1.py`:
- Around line 1859-1864: Update the team ID selection logic around
_resolve_write_team_id so an omitted team_id never falls back to ctx.team_id.
When team_id is absent, always inherit the team_id from current_active_version
or existing_versions[0], preserving explicit team_id resolution through
_resolve_write_team_id.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3eb73a8c-9806-44ef-a1a7-69f7133a14d1

📥 Commits

Reviewing files that changed from the base of the PR and between 654c5c8 and 737cdc3.

📒 Files selected for processing (2)
  • autogpt_platform/backend/backend/api/features/v1.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
📜 Review details
⏰ Context from checks skipped due to timeout. (15)
  • GitHub Check: integration_test
  • GitHub Check: lint
  • GitHub Check: check API types
  • GitHub Check: Seer Code Review
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.12)
  • GitHub Check: type-check (3.13)
  • GitHub Check: Check PR Status
  • GitHub Check: type-check (3.11)
  • GitHub Check: test (3.13)
  • GitHub Check: end-to-end tests
  • GitHub Check: type-check (3.12)
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (typescript)
  • GitHub Check: copilot-setup-steps
🧰 Additional context used
📓 Path-based instructions (5)
autogpt_platform/backend/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

autogpt_platform/backend/**/*.py: Use Python 3.11 (required; managed by Poetry via pyproject.toml) for backend development
Always run 'poetry run format' (Black + isort) before linting in backend development
Always run 'poetry run lint' (ruff) after formatting in backend development

autogpt_platform/backend/**/*.py: Use poetry run ... command for executing Python package dependencies
Use top-level imports only — avoid local/inner imports except for lazy imports of heavy optional dependencies like openpyxl
Use absolute imports with from backend.module import ... for cross-package imports; single-dot relative imports are acceptable for sibling modules within the same package; avoid double-dot relative imports
Do not use duck typing — avoid hasattr/getattr/isinstance for type dispatch; use typed interfaces/unions/protocols instead
Use Pydantic models over dataclass/namedtuple/dict for structured data
Do not use linter suppressors — no # type: ignore, # noqa, # pyright: ignore; fix the type/code instead
Prefer list comprehensions over manual loop-and-append patterns
Use early return with guard clauses first to avoid deep nesting
Use %s for deferred interpolation in debug log statements for efficiency; use f-strings elsewhere for readability (e.g., logger.debug("Processing %s items", count) vs logger.info(f"Processing {count} items"))
Sanitize error paths by using os.path.basename() in error messages to avoid leaking directory structure
Be aware of TOCTOU (Time-Of-Check-Time-Of-Use) issues — avoid check-then-act patterns for file access and credit charging
Use transaction=True for Redis pipelines to ensure atomicity on multi-step operations
Use max(0, value) guards for computed values that should never be negative
Keep files under ~300 lines; if a file grows beyond this, split by responsibility (extract helpers, models, or a sub-module into a new file)
Keep functions under ~40 lines; extract named helpers when a function grows longer
...

Files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
autogpt_platform/backend/backend/api/features/**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Update routes in '/backend/backend/api/features/' and add/update Pydantic models in the same directory for API development

Files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
autogpt_platform/{backend,autogpt_libs}/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Format Python code with poetry run format

Files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
autogpt_platform/backend/**/api/**/*.py

📄 CodeRabbit inference engine (autogpt_platform/backend/AGENTS.md)

autogpt_platform/backend/**/api/**/*.py: Use Security() instead of Depends() for authentication dependencies to get proper OpenAPI security specification
Follow SSE (Server-Sent Events) protocol: use data: lines for frontend-parsed events (must match Zod schema) and : comment lines for heartbeats/status

Files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
autogpt_platform/backend/**/*_test.py

📄 CodeRabbit inference engine (autogpt_platform/backend/AGENTS.md)

autogpt_platform/backend/**/*_test.py: Use pytest with snapshot testing for API responses
Colocate test files with source files using *_test.py naming convention
Mock at boundaries — mock where the symbol is used, not where it's defined; after refactoring, update mock targets to match new module paths
Use AsyncMock from unittest.mock for async functions in tests
When writing tests, use Test-Driven Development (TDD): write failing tests marked with @pytest.mark.xfail before implementation, then remove the marker once the implementation is complete
When creating snapshots in tests, use poetry run pytest path/to/test.py --snapshot-update; always review snapshot changes with git diff before committing

Files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
🧠 Learnings (11)
📚 Learning: 2026-02-26T17:02:22.448Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12211
File: .pre-commit-config.yaml:160-179
Timestamp: 2026-02-26T17:02:22.448Z
Learning: Keep the pre-commit hook pattern broad for autogpt_platform/backend to ensure OpenAPI schema changes are captured. Do not narrow to backend/api/ alone, since the generated schema depends on Pydantic models across multiple directories (backend/data/, backend/blocks/, backend/copilot/, backend/integrations/, backend/util/). Narrowing could miss schema changes and cause frontend type desynchronization.

Applied to files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-03-05T15:42:08.207Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12297
File: .claude/skills/backend-check/SKILL.md:14-16
Timestamp: 2026-03-05T15:42:08.207Z
Learning: In Python files under autogpt_platform/backend (recursively), rely on poetry run format to perform formatting (Black + isort) and linting (ruff). Do not run poetry run lint as a separate step after poetry run format, since format already includes linting checks.

Applied to files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-03-16T16:35:40.236Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12440
File: autogpt_platform/backend/backend/api/features/workflow_import.py:54-63
Timestamp: 2026-03-16T16:35:40.236Z
Learning: Avoid using the word 'competitor' in public-facing identifiers and text. Use neutral naming for API paths, model names, function names, and UI text. Examples: rename 'CompetitorFormat' to 'SourcePlatform', 'convert_competitor_workflow' to 'convert_workflow', '/competitor-workflow' to '/workflow'. Apply this guideline to files under autogpt_platform/backend and autogpt_platform/frontend.

Applied to files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-03-31T15:37:38.626Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12623
File: autogpt_platform/backend/backend/copilot/tools/agent_generator/fixer.py:37-47
Timestamp: 2026-03-31T15:37:38.626Z
Learning: When validating/constructing Anthropic API model IDs in Significant-Gravitas/AutoGPT, allow the hyphen-separated Claude Opus 4.6 model ID `claude-opus-4-6` (it corresponds to `LlmModel.CLAUDE_4_6_OPUS` in `autogpt_platform/backend/backend/blocks/llm.py`). Do NOT require the dot-separated form in Anthropic contexts. Only OpenRouter routing variants should use the dot separator (e.g., `anthropic/claude-opus-4.6`); `claude-opus-4-6` should be treated as correct when passed to Anthropic, and flagged only if it’s used in the OpenRouter path where the dot form is expected.

Applied to files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-04-15T02:43:36.890Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12780
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:0-0
Timestamp: 2026-04-15T02:43:36.890Z
Learning: When reviewing Python exception handlers, do not flag `isinstance(e, X)` checks as dead/unreachable if the caught exception `X` is a subclass of the exception type being handled. For example, if `X` (e.g., `VirusScanError`) inherits from `ValueError` (directly or via an intermediate class) and it can be raised within an `except ValueError:` block, then `isinstance(e, X)` inside that handler is reachable and should not be treated as dead code.

Applied to files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-05-23T05:29:43.085Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 13200
File: autogpt_platform/backend/backend/executor/scheduler.py:590-593
Timestamp: 2026-05-23T05:29:43.085Z
Learning: When reviewing Python code that uses Pydantic discriminated/tagged unions (e.g., `Annotated[Union[...], Field(discriminator="kind")]`), recognize that using `isinstance(x, SomeVariantInfo)` to narrow the union is an intentional and correct runtime guard and should also enable static type narrowing in tools like Pyright. Do not recommend replacing such `isinstance`-based narrowing with `cast(...)` when the check already proves the variant at runtime.

Applied to files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-04-22T11:46:04.431Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12881
File: autogpt_platform/backend/backend/copilot/config.py:0-0
Timestamp: 2026-04-22T11:46:04.431Z
Learning: Do not flag the Claude Sonnet 4.6 model ID as incorrect when it uses the project’s established hyphenated convention: `anthropic/claude-sonnet-4-6`. This hyphen form is the intentional, production convention and should be treated as valid (including in files like llm.py, blocks tests, reasoning.py, `_is_anthropic_model` tests, and config defaults). Note that OpenRouter also accepts the dot variant `anthropic/claude-sonnet-4.6`, so either form may be tolerated, but `anthropic/claude-sonnet-4-6` should be considered the standard to match project usage.

Applied to files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-04-22T11:46:12.892Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12881
File: autogpt_platform/backend/backend/copilot/baseline/service.py:322-332
Timestamp: 2026-04-22T11:46:12.892Z
Learning: In this codebase (Significant-Gravitas/AutoGPT), OpenRouter-routed Anthropic model IDs should use the hyphen-separated convention (e.g., `anthropic/claude-sonnet-4-6`, `anthropic/claude-opus-4-6`). Although OpenRouter may accept both hyphen and dot variants, treat the hyphen-separated form as the intended, correct codebase-wide convention and do not flag it as an error. Only flag the dot-separated variant (e.g., `anthropic/claude-sonnet-4.6`) as incorrect when reviewing/validating model ID strings for OpenRouter-routed Anthropic models.

Applied to files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-05-07T18:48:14.242Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 13040
File: autogpt_platform/backend/backend/blocks/llm.py:0-0
Timestamp: 2026-05-07T18:48:14.242Z
Learning: In this repository, isort may split imports from the same module into separate blocks when some imports are aliased (e.g., `from module import X as Y`) and others are not. Preserve the two-block layout when it results from isort (such as keeping `from openai.types.chat import ChatCompletion as OpenAIChatCompletion` separate from non-aliased imports from `openai.types.chat`). Do not treat that split as a style issue during review; merging them into a single block can fail CI with `Imports are incorrectly sorted and/or formatted`.

Applied to files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-05-26T14:24:34.866Z
Learnt from: Abhi1992002
Repo: Significant-Gravitas/AutoGPT PR: 13217
File: autogpt_platform/backend/backend/api/features/search/service.py:137-137
Timestamp: 2026-05-26T14:24:34.866Z
Learning: In the Significant-Gravitas/AutoGPT backend, treat `user_id` (an opaque UUID used only for correlation/tracing) as non-PII. Do not flag direct logging of `user_id` in `logger.warning`/`logger.info` statements as a PII exposure issue, as the established convention is to log `user_id` for tracing while reserving PII for fields like email or display name.

Applied to files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
📚 Learning: 2026-06-11T19:39:10.493Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 13337
File: autogpt_platform/backend/backend/copilot/graphiti/reranker.py:0-0
Timestamp: 2026-06-11T19:39:10.493Z
Learning: In the Significant-Gravitas/AutoGPT Python backend, when calling the OpenAI Python client `chat.completions.create`, construct the `messages` payload using the concrete typed-dict variants from `openai.types.chat` (e.g., `ChatCompletionSystemMessageParam`, `ChatCompletionUserMessageParam`, etc.) rather than trying to instantiate `ChatCompletionMessageParam` directly. `ChatCompletionMessageParam` is a `Union` alias and is not constructible, so `ChatCompletionMessageParam(role=..., content=...)` should fail type checking. Build each message element with the appropriate concrete typed dict and then annotate the resulting list as `list[ChatCompletionMessageParam]` (e.g., `messages: list[ChatCompletionMessageParam] = [ChatCompletionSystemMessageParam(...), ...]`).

Applied to files:

  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/api/features/v1.py
🪛 Ruff (0.16.0)
autogpt_platform/backend/backend/api/features/v1.py

[warning] 210-210: Do not perform function call Security in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🔇 Additional comments (1)
autogpt_platform/backend/backend/api/features/v1_test.py (1)

120-139: 📐 Maintainability & Code Quality | ⚡ Quick win

Snapshot the created-user response body.

This path only checks the header; add a response snapshot to cover the created-user payload as well. As per coding guidelines, “Use pytest with snapshot testing for API responses.”

[ suggest_recommended_refactor ]

Source: Coding guidelines

@ntindle

ntindle commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

/review

@autogpt-pr-reviewer

Copy link
Copy Markdown

Queued a review for PR #13540 at 737cdc3.

@autogpt-pr-reviewer autogpt-pr-reviewer 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.

📋 Automated Review — PR #13540

PR #13540 — feat(backend): accept explicit team_id on create/save flows
Author: ntindle | Files: 4

🎯 Verdict: APPROVE

PR Description Quality

✅ Has Why + What + How — clearly frames the re-save bug (SECRT-2462), the four write surfaces touched, and the validation rule. Author's checklist is fully filled with a concrete test plan.

What This PR Does

Previously, saving an agent from the builder (which no longer sends team context) silently reset a team-owned agent back to org-home, because the write path defaulted a missing team_id to NULL. This PR lets all four create/save surfaces (POST /graphs, PUT /graphs/{id}, POST /graphs/{id}/schedules, POST /api-keys) accept an explicit team_id, validates it through a single shared helper (_resolve_write_team_id) that requires the caller to be an ACTIVE member of a team in their org, and — on update/schedule — inherits the existing tenant when team_id is omitted so a re-save no longer drops the agent to org-home.

Specialist Findings

🛡️ Security ✅ — Traced the full trust chain. _resolve_write_team_id delegates to get_user_team_ids, which filters on both status: "ACTIVE" and Team.orgId == organization_id, so non-member, stale, and cross-org teams all fail closed with a 400 before any persistence. Net security improvement over the retired header. One 🟡 defense-in-depth note: the inherit path (v1.py:1864, :2543) doesn't re-check the inherited team_id against the current org, so a multi-org user switching X-Org-Id between saves could produce a version whose organization_id and team_id belong to different orgs. Self-owned resource only, no cross-user exposure.

🏗️ Architecture ✅ — Clean, tightly-scoped. Single enforcement point reused across all four surfaces; data layer untouched, so blast radius is just route stamping. 🔵 Notes: team_id is a query param on PUT /graphs/{id} but a body field elsewhere (v1.py:1833); comment at v1.py:1854 uses change-relative "the key fix / now always absent" language that won't age well post-merge.

Performance ✅ — Adds exactly one bounded, index-backed membership query per write, and only when a client supplies team_id (the common omitted-path skips it entirely, verified by test). O(1) extra round trips, no N+1. 🟡 Optional micro-opt (tenancy.py:26): a targeted find_first existence check would avoid fetching the full membership set, immaterial at real team cardinality.

🧪 Testing ⚠️ — Graph-create and api-key flows are well covered (valid / omitted / non-member / cross-org), and the re-save inherit fix has a dedicated test. Gap: create_graph_execution_schedule gained new team-resolution logic with zero tests (v1.py:2534), and update_graph lacks a non-member 400 test on its query-param wiring plus a no-active-version fallback test (v1.py:1863). (Flagged by: testing, discussion — 2 specialists.)

📖 Quality ✅ — Grade A: clear naming, good docstrings, tests carry intent docstrings. 🔵 The explicit→context→inherit ladder is copy-pasted across three sites (v1.py:1857, ~2534, ~1766) and could drift; str | None vs Optional[str] style is mixed within the PR.

📦 Product ✅ — Backend slice matches requirements exactly. 🟡 Forward-looking gaps for the follow-up frontend picker: team_id=None is overloaded to mean "inherit" on update, so there's no wire value meaning "move a team agent back to org-home" (v1.py:1858); and the query-vs-body inconsistency will force the picker to special-case PUT.

📬 Discussion ⚠️ — GitHub CI reported 47/47 required checks green at head 737cdc3; no merge conflicts (MERGEABLE). Two open CodeRabbit (Major) threads with no author reply: the missing schedule-flow team tests, and the elif ctx.team_id is not None fallback (documented by the author as dead "now always-null" back-compat). No human approval yet; author self-triggered a fresh /review on 2026-08-06 that was still pending.

🔎 QA ✅ — Exercised all four surfaces live against the running stack with before/after DB verification. 12/12 scenarios Actual = Expected: explicit team stamped on both AgentGraph and LibraryAgent; invalid/non-member/cross-org → 400 with nothing persisted; unauthenticated → 401. The headline re-save fix was confirmed against live DB — re-saving with no team context produced v2.teamId = 8f7842f5… (inherited) instead of resetting to NULL.

🟠 Should Fix

  1. Add schedule-endpoint team-resolution tests (v1.py:2534) — the schedule surface gained explicit-validate / omit-inherit / reject logic with no tests; this is the exact silent-misplacement class of bug the PR fixes for graphs. Add: explicit-valid stamp, omitted→graph.team_id, non-member→400. (Flagged by: testing, discussion — 2 specialists.)
  2. Add update_graph rejection + fallback tests (v1.py:1833, :1863) — no test covers a non-member ?team_id= returning 400 on the query-param path, and the existing_versions[0] (no-active-version) inherit branch is never exercised. (Flagged by: testing.)

🟡 Nice to Have

  1. Re-validate inherited team_id against current org on inherit path (v1.py:1864, :2543) — defense-in-depth against org/team mismatch for multi-org users. (security)
  2. Resolve the "move back to org-home" gap and query-vs-body shape before the frontend picker locks the contract (v1.py:1858, :1833) — cheaper to decide now than a breaking change later. (product, architect — 2 specialists)
  3. Targeted membership existence check (tenancy.py:26) instead of fetch-all-and-scan. (performance)

🔵 Nits

  1. Non-durable diff-relative comment (v1.py:1854) — rewrite as a standing invariant. (architect)
  2. Mixed str | None vs Optional[str] within the PR (v1.py:1725). (quality)
  3. Collapse the duplicated resolution ladder into the helper (v1.py:1766, 1857, 2534). (quality, architect — 2 specialists)

QA Screenshots

Screenshot Description
library render Authenticated app renders after test run ✅ (backend-only PR; no UI in this diff)

Human Review Needed

YES — this modifies an authorization/tenancy trust boundary (which teams a caller may stamp resources into); a maintainer familiar with the org/team model should eyeball the inherit-path org-consistency note before merge.

Risk Assessment

Merge risk: LOW | Rollback: EASY — additive, backward-compatible (all new fields Optional = None), data layer untouched, fail-closed on invalid input.

CI Status

GitHub CI: per the discussion specialist, 47/47 required checks were green at head 737cdc3 (not independently re-fetched this run). Local harness: backend lint ✅, frontend lint ✅, typecheck ✅, build ✅; the frontend pnpm test:unit suite failed in the sandbox — this is a frontend suite unrelated to a backend-only diff and reflects environment skew, not a regression from this PR.


UI Testing — Variant Results

✅ local: All four create/save surfaces correctly validate and stamp explicit team_id, reject invalid/cross-org teams with 400, and the PUT re-save bug fix (inherit existing tenant) is verified against live DB state.

✅ hosted: All four team_id surfaces (graph create, graph save, schedule, api-key) resolve tenancy correctly and reject non-member teams; the re-save inheritance regression fix is confirmed live in the DB.

elif ctx.team_id is not None:
resolved_team_id = ctx.team_id
else:
resolved_team_id = (current_active_version or existing_versions[0]).team_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.

🤖 🟢 low (security/tenancy-consistency)

When team_id is omitted and no header team context is set, update_graph inherits the prior version's team_id without verifying it belongs to ctx.org_id. A multi-org user who switches X-Org-Id between saves can create a version whose organization_id and team_id belong to different orgs, which could confuse team-keyed access checks.

Suggestion: Validate the inherited team_id is in get_user_team_ids(user_id, ctx.org_id); if it isn't (org changed), clear to org-home (None) or reject, so organization_id and team_id always stay in the same org.

elif ctx.team_id is not None:
resolved_team_id = ctx.team_id
else:
resolved_team_id = graph.team_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.

🤖 🟢 low (security/tenancy-consistency)

create_graph_execution_schedule inherits graph.team_id with no check that the team belongs to the current ctx.org_id, so a schedule can be stamped with a team from a different org than the request's org context for a multi-org user.

Suggestion: Apply the same org-consistency check to the inherited graph.team_id as used on the explicit path (membership within ctx.org_id), else fall back to org-home.

graph.version = max(g.version for g in existing_versions) + 1
current_active_version = next((v for v in existing_versions if v.is_active), None)

# Team resolution for the new version: an explicit picker choice wins,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 🟢 low (architect/comment-durability)

The update-graph resolution comment is diff-justification with change-relative language ('the key fix for the observed bug', 'team_id now always absent'). After merge it documents a bug that no longer exists and depends on knowing when the diff landed.

Suggestion: Rewrite to state the standing invariant only: explicit choice wins, then active-team context, else inherit the existing tenant so a team-owned agent stays in its team on re-save.

graph: graph_db.Graph,
user_id: Annotated[str, Security(get_user_id)],
ctx: Annotated[RequestContext, Security(get_request_context)],
team_id: Annotated[

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 🟢 low (architect/api-consistency)

team_id is a query param on PUT /graphs/{id} but a body field on create, schedule, and api-key surfaces. The frontend picker will have to special-case this one surface.

Suggestion: Prefer a consistent placement across the four write surfaces, or document the intentional divergence for the follow-up frontend slice.

# otherwise inherit the agent's existing tenant. Inheriting is the key
# fix for the observed bug — without it, re-saving from the builder
# (team_id now always absent) silently moves a team agent to org-home.
if team_id is not None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 🟢 low (architect/design-asymmetry)

On create, omitted team_id means org-home; on update, omitted means inherit existing tenant. This correctly fixes the re-save bug but leaves no way to move a team-owned graph back to org-home via the API.

Suggestion: If demote-to-org-home is a needed action, plan a sentinel value or dedicated path; otherwise note the deferral explicitly.

detail=f"Graph #{graph_id} v{schedule_params.graph_version} not found.",
)

# A schedule is bound to a graph, so it inherits the graph's tenant unless

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 🟡 medium (product/ux-gap)

Schedule creation inherits the graph's team when team_id is omitted, with no way to create a schedule in org-home for a team-owned graph — same sentinel-overload limitation as PUT /graphs.

Suggestion: Apply the same explicit org-home sentinel decision here for consistency across surfaces.

graph: graph_db.Graph,
user_id: Annotated[str, Security(get_user_id)],
ctx: Annotated[RequestContext, Security(get_request_context)],
team_id: Annotated[

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 🟡 medium (product/api-consistency)

team_id is a query parameter on PUT /graphs/{id} but a request-body field on POST /graphs, POST /schedules, and POST /api-keys. The shared frontend picker must special-case this one surface.

Suggestion: Move team_id into the update request body for uniformity, or document explicitly why PUT must use a query param.

# then the (validated) active-team context if a legacy header set it,
# otherwise inherit the agent's existing tenant. Inheriting is the key
# fix for the observed bug — without it, re-saving from the builder
# (team_id now always absent) silently moves a team agent to org-home.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 🟢 low (product/backward-compat / docs)

The corrected PUT /graphs inherit-on-omit behavior is an observable API contract change for existing consumers that relied on the old reset-to-org-home behavior, but there is no changelog/migration note.

Suggestion: Add a release-note/changelog line documenting that re-saving a team-owned agent now preserves its team.

resolved_team_id = await _resolve_write_team_id(
user_id, ctx.org_id, schedule_params.team_id
)
elif ctx.team_id is not None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 🟡 medium (discussion/unaddressed-reviewer-concern / test-coverage)

CodeRabbit (Major) flagged that the POST /graphs/{id}/schedules team resolution has zero test coverage. The schedule-specific inheritance path (resolved_team_id = graph.team_id) is security-sensitive since schedules auto-execute under the resolved team, yet no test asserts explicit-team forwarding, omitted->graph.team_id inheritance, or 400 on invalid team. The PR description implies schedule scenarios are tested, but the diff only covers create/update/api-key. No author response.

Suggestion: Add the three suggested tests for the schedule flow (explicit valid team_id forwarded to add_execution_schedule, omitted inherits graph.team_id, invalid returns 400 without calling scheduler), or reply on the thread explaining the gap.

# fix for the observed bug — without it, re-saving from the builder
# (team_id now always absent) silently moves a team agent to org-home.
if team_id is not None:
resolved_team_id = await _resolve_write_team_id(user_id, ctx.org_id, team_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.

🤖 🟡 medium (discussion/unaddressed-reviewer-concern)

CodeRabbit (Major, outside-diff) flagged that when team_id is omitted the code falls back to ctx.team_id (elif ctx.team_id is not None) before inheriting the existing/active version's team, bypassing _resolve_write_team_id validation. The same pattern appears in the schedule and create paths. The author's inline comment states ctx.team_id is 'now always-null' back-compat, but the reviewer's point that this branch is unvalidated and could silently re-stamp tenant was never acknowledged or resolved.

Suggestion: Either remove the dead ctx.team_id fallback so omitted team_id always inherits the existing tenant, or reply on the thread confirming why the back-compat branch is safe to keep.

@github-actions github-actions Bot added cla: signed CLA signed by all contributors conflicts Automatically applied to PRs with merge conflicts labels Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

This pull request has conflicts with the base branch, please resolve those so we can evaluate the pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

batch:orgs batch-bot batch membership batch PR is queued in the batch-deploy rollup (batch-bot source of truth) cla: signed CLA signed by all contributors conflicts Automatically applied to PRs with merge conflicts platform/backend AutoGPT Platform - Back end platform/frontend AutoGPT Platform - Front end size/l

Projects

Status: 🆕 Needs initial review
Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant