From 5dae303ce0a2080b06a971b90b7aaeb6dc4ccb91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 06:42:47 +0000 Subject: [PATCH 1/3] chore(frontend/deps): Bump react-window and @types/react-window in /autogpt_platform/frontend (#10943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [react-window](https://github.com/bvaughn/react-window) and [@types/react-window](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-window). These dependencies needed to be updated together. Updates `react-window` from 1.8.11 to 2.1.0
Release notes

Sourced from react-window's releases.

2.1.0

Improved ARIA support:

// Example of how to use new `ariaAttributes` prop
function RowComponent({
  ariaAttributes,
  index,
  style,
  ...rest
}: RowComponentProps<object>) {
  return (
    <div style={style} {...ariaAttributes}>
      ...
    </div>
  );
}

Added optional children prop to better support edge cases like sticky rows.

Minor changes to onRowsRendered and onCellsRendered callbacks to make it easier to differentiate between visible items and items rendered due to overscan settings. These methods will now receive two params– the first for visible rows and the second for all rows (including overscan), e.g.:

function onRowsRendered(
  visibleRows: {
    startIndex: number;
    stopIndex: number;
  },
  allRows: {
    startIndex: number;
    stopIndex: number;
  }
): void {
  // ...
}

function onCellsRendered(
visibleCells: {
columnStartIndex: number;
columnStopIndex: number;
rowStartIndex: number;
rowStopIndex: number;
</tr></table>

... (truncated)

Changelog

Sourced from react-window's changelog.

2.1.0

Improved ARIA support:

// Example of how to use new `ariaAttributes` prop
function RowComponent({
  ariaAttributes,
  index,
  style,
  ...rest
}: RowComponentProps<object>) {
  return (
    <div style={style} {...ariaAttributes}>
      ...
    </div>
  );
}

Added optional children prop to better support edge cases like sticky rows.

Minor changes to onRowsRendered and onCellsRendered callbacks to make it easier to differentiate between visible items and items rendered due to overscan settings. These methods will now receive two params– the first for visible rows and the second for all rows (including overscan), e.g.:

function onRowsRendered(
  visibleRows: {
    startIndex: number;
    stopIndex: number;
  },
  allRows: {
    startIndex: number;
    stopIndex: number;
  }
): void {
  // ...
}

function onCellsRendered(
visibleCells: {
columnStartIndex: number;
columnStopIndex: number;
rowStartIndex: number;
</tr></table>

... (truncated)

Commits

Updates `@types/react-window` from 1.8.8 to 2.0.0
Commits

You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nicholas Tindle Co-authored-by: Nick Tindle --- autogpt_platform/frontend/package.json | 4 +- autogpt_platform/frontend/pnpm-lock.yaml | 38 +++++++++---------- .../ActivityDropdown/ActivityDropdown.tsx | 31 +++++++-------- 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/autogpt_platform/frontend/package.json b/autogpt_platform/frontend/package.json index f22a182d20..e8c9871a72 100644 --- a/autogpt_platform/frontend/package.json +++ b/autogpt_platform/frontend/package.json @@ -102,7 +102,7 @@ "react-markdown": "9.0.3", "react-modal": "3.16.3", "react-shepherd": "6.1.9", - "react-window": "1.8.11", + "react-window": "2.2.0", "recharts": "3.3.0", "rehype-autolink-headings": "7.1.0", "rehype-highlight": "7.0.2", @@ -140,7 +140,7 @@ "@types/react": "18.3.17", "@types/react-dom": "18.3.5", "@types/react-modal": "3.16.3", - "@types/react-window": "1.8.8", + "@types/react-window": "2.0.0", "@vitejs/plugin-react": "5.1.2", "axe-playwright": "2.2.2", "chromatic": "13.3.3", diff --git a/autogpt_platform/frontend/pnpm-lock.yaml b/autogpt_platform/frontend/pnpm-lock.yaml index db891ccf3f..377a298564 100644 --- a/autogpt_platform/frontend/pnpm-lock.yaml +++ b/autogpt_platform/frontend/pnpm-lock.yaml @@ -228,8 +228,8 @@ importers: specifier: 6.1.9 version: 6.1.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) react-window: - specifier: 1.8.11 - version: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 2.2.0 + version: 2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) recharts: specifier: 3.3.0 version: 3.3.0(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(redux@5.0.1) @@ -337,8 +337,8 @@ importers: specifier: 3.16.3 version: 3.16.3 '@types/react-window': - specifier: 1.8.8 - version: 1.8.8 + specifier: 2.0.0 + version: 2.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@vitejs/plugin-react': specifier: 5.1.2 version: 5.1.2(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) @@ -3469,8 +3469,9 @@ packages: '@types/react-modal@3.16.3': resolution: {integrity: sha512-xXuGavyEGaFQDgBv4UVm8/ZsG+qxeQ7f77yNrW3n+1J6XAstUy5rYHeIHPh1KzsGc6IkCIdu6lQ2xWzu1jBTLg==} - '@types/react-window@1.8.8': - resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} + '@types/react-window@2.0.0': + resolution: {integrity: sha512-E8hMDtImEpMk1SjswSvqoSmYvk7GEtyVaTa/GJV++FdDNuMVVEzpAClyJ0nqeKYBrMkGiyH6M1+rPLM0Nu1exQ==} + deprecated: This is a stub types definition. react-window provides its own type definitions, so you do not need this installed. '@types/react@18.3.17': resolution: {integrity: sha512-opAQ5no6LqJNo9TqnxBKsgnkIYHozW9KSTlFVoSUJYh1Fl/sswkEoqIugRSm7tbh6pABtYjGAjW+GOS23j8qbw==} @@ -5976,9 +5977,6 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} - memoize-one@5.2.1: - resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -6891,12 +6889,11 @@ packages: '@types/react': optional: true - react-window@1.8.11: - resolution: {integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==} - engines: {node: '>8.0.0'} + react-window@2.2.0: + resolution: {integrity: sha512-Y2L7yonHq6K1pQA2P98wT5QdIsEcjBTB7T8o6Mub12hH9eYppXoYu6vgClmcjlh3zfNcW2UrXiJJJqDxUY7GVw==} peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} @@ -11603,9 +11600,12 @@ snapshots: dependencies: '@types/react': 18.3.17 - '@types/react-window@1.8.8': + '@types/react-window@2.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@types/react': 18.3.17 + react-window: 2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - react + - react-dom '@types/react@18.3.17': dependencies: @@ -14545,8 +14545,6 @@ snapshots: dependencies: fs-monkey: 1.1.0 - memoize-one@5.2.1: {} - merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -15592,10 +15590,8 @@ snapshots: optionalDependencies: '@types/react': 18.3.17 - react-window@1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-window@2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.28.4 - memoize-one: 5.2.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) diff --git a/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/components/ActivityDropdown/ActivityDropdown.tsx b/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/components/ActivityDropdown/ActivityDropdown.tsx index 263453b327..885877786f 100644 --- a/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/components/ActivityDropdown/ActivityDropdown.tsx +++ b/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/components/ActivityDropdown/ActivityDropdown.tsx @@ -4,7 +4,7 @@ import { Button } from "@/components/atoms/Button/Button"; import { Input } from "@/components/atoms/Input/Input"; import { Text } from "@/components/atoms/Text/Text"; import { Bell, MagnifyingGlass, X } from "@phosphor-icons/react"; -import { FixedSizeList as List } from "react-window"; +import { List, type RowComponentProps } from "react-window"; import { AgentExecutionWithInfo } from "../../helpers"; import { ActivityItem } from "../ActivityItem"; import styles from "./styles.module.css"; @@ -19,14 +19,16 @@ interface Props { recentFailures: AgentExecutionWithInfo[]; } -interface VirtualizedItemProps { - index: number; - style: React.CSSProperties; - data: AgentExecutionWithInfo[]; +interface ActivityRowProps { + executions: AgentExecutionWithInfo[]; } -function VirtualizedActivityItem({ index, style, data }: VirtualizedItemProps) { - const execution = data[index]; +function VirtualizedActivityItem({ + index, + style, + executions, +}: RowComponentProps) { + const execution = executions[index]; return (
@@ -129,14 +131,13 @@ export function ActivityDropdown({ > {filteredExecutions.length > 0 ? ( - {VirtualizedActivityItem} - + defaultHeight={listHeight} + rowCount={filteredExecutions.length} + rowHeight={itemHeight} + rowProps={{ executions: filteredExecutions }} + rowComponent={VirtualizedActivityItem} + style={{ width: 320, height: listHeight }} + /> ) : (
From 1a16e203b8df62efd1fc8dfeb83d5f795385b445 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 07:11:21 +0000 Subject: [PATCH 2/3] chore(deps): Bump actions/setup-node from 4 to 6 (#11213) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
Release notes

Sourced from actions/setup-node's releases.

v6.0.0

What's Changed

Breaking Changes

Dependency Upgrades

Full Changelog: https://github.com/actions/setup-node/compare/v5...v6.0.0

v5.0.0

What's Changed

Breaking Changes

This update, introduces automatic caching when a valid packageManager field is present in your package.json. This aims to improve workflow performance and make dependency management more seamless. To disable this automatic caching, set package-manager-cache: false

steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
  with:
    package-manager-cache: false

Make sure your runner is on version v2.327.1 or later to ensure compatibility with this release. See Release Notes

Dependency Upgrades

New Contributors

Full Changelog: https://github.com/actions/setup-node/compare/v4...v5.0.0

v4.4.0

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=4&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nick Tindle --- .github/workflows/claude-dependabot.yml | 2 +- .github/workflows/claude.yml | 2 +- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/platform-frontend-ci.yml | 10 +++++----- .github/workflows/platform-fullstack-ci.yml | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/claude-dependabot.yml b/.github/workflows/claude-dependabot.yml index c39fdb0e35..6dbe068c3d 100644 --- a/.github/workflows/claude-dependabot.yml +++ b/.github/workflows/claude-dependabot.yml @@ -78,7 +78,7 @@ jobs: # Frontend Node.js/pnpm setup (mirrors platform-frontend-ci.yml) - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22" diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 805f3d78bb..8e165b823e 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -94,7 +94,7 @@ jobs: # Frontend Node.js/pnpm setup (mirrors platform-frontend-ci.yml) - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22" diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index f70fe36572..eae6eea5d2 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -76,7 +76,7 @@ jobs: # Frontend Node.js/pnpm setup (mirrors platform-frontend-ci.yml) - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22" diff --git a/.github/workflows/platform-frontend-ci.yml b/.github/workflows/platform-frontend-ci.yml index 01e057207d..669a775934 100644 --- a/.github/workflows/platform-frontend-ci.yml +++ b/.github/workflows/platform-frontend-ci.yml @@ -42,7 +42,7 @@ jobs: - 'autogpt_platform/frontend/src/components/**' - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22.18.0" @@ -74,7 +74,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22.18.0" @@ -112,7 +112,7 @@ jobs: fetch-depth: 0 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22.18.0" @@ -153,7 +153,7 @@ jobs: submodules: recursive - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22.18.0" @@ -282,7 +282,7 @@ jobs: submodules: recursive - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22.18.0" diff --git a/.github/workflows/platform-fullstack-ci.yml b/.github/workflows/platform-fullstack-ci.yml index f64d5e33c9..67be0ae939 100644 --- a/.github/workflows/platform-fullstack-ci.yml +++ b/.github/workflows/platform-fullstack-ci.yml @@ -32,7 +32,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22.18.0" @@ -68,7 +68,7 @@ jobs: submodules: recursive - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22.18.0" From e8fc8ee6234772484e4c0f88ffa1e4d019e6e488 Mon Sep 17 00:00:00 2001 From: Nicholas Tindle Date: Mon, 9 Feb 2026 01:19:43 -0600 Subject: [PATCH 3/3] fix(backend): filter graph-only blocks from CoPilot's find_block results (#11892) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filters out blocks that are unsuitable for standalone execution from CoPilot's block search and execution. These blocks serve graph-specific purposes and will either fail, hang, or confuse users when run outside of a graph context. **Important:** This does NOT affect the Builder UI which uses `load_all_blocks()` directly. ### Changes 🏗️ - **find_block.py**: Added `EXCLUDED_BLOCK_TYPES` and `EXCLUDED_BLOCK_IDS` constants, skip excluded blocks in search results - **run_block.py**: Added execution guard that returns clear error message for excluded blocks - **content_handlers.py**: Added filtering to `BlockHandler.get_missing_items()` and `get_stats()` to prevent indexing excluded blocks **Excluded by BlockType:** | BlockType | Reason | |-----------|--------| | `INPUT` | Graph interface definition - data enters via chat, not graph inputs | | `OUTPUT` | Graph interface definition - data exits via chat, not graph outputs | | `WEBHOOK` | Wait for external events - would hang forever in CoPilot | | `WEBHOOK_MANUAL` | Same as WEBHOOK | | `NOTE` | Visual annotation only - no runtime behavior | | `HUMAN_IN_THE_LOOP` | Pauses for human approval - CoPilot IS human-in-the-loop | | `AGENT` | AgentExecutorBlock requires graph context - use `run_agent` tool instead | **Excluded by ID:** | Block | Reason | |-------|--------| | `SmartDecisionMakerBlock` | Dynamically discovers downstream blocks via graph topology | ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [ ] Search for "input" in CoPilot - should NOT return AgentInputBlock variants - [ ] Search for "output" in CoPilot - should NOT return AgentOutputBlock - [ ] Search for "webhook" in CoPilot - should NOT return trigger blocks - [ ] Search for "human" in CoPilot - should NOT return HumanInTheLoopBlock - [ ] Search for "decision" in CoPilot - should NOT return SmartDecisionMakerBlock - [ ] Verify functional blocks still appear (e.g., "email", "http", "text") - [ ] Verify Builder UI still shows ALL blocks (no regression) #### For configuration changes: - [x] `.env.default` is updated or already compatible with my changes - [x] `docker-compose.yml` is updated or already compatible with my changes - [x] I have included a list of my configuration changes in the PR description (under **Changes**) No configuration changes required. --- Resolves: [SECRT-1831](https://linear.app/autogpt/issue/SECRT-1831) 🤖 Generated with [Claude Code](https://claude.ai/code) --- > [!NOTE] > **Low Risk** > Behavior change is limited to CoPilot’s block discovery/execution guards and is covered by new tests; main risk is inadvertently excluding a block that should be runnable. > > **Overview** > CoPilot now **filters out graph-only blocks** from `find_block` results and prevents them from being executed via `run_block`, returning a clear error when a user attempts to run an excluded block. > > `find_block` introduces explicit exclusion lists (by `BlockType` and a specific block ID), over-fetches search results to maintain up to 10 usable matches after filtering, and adds debug logging when results are reduced. New unit tests cover both the search filtering and the `run_block` execution guard; a minor cleanup removes an unused `pytest` import in `execution_queue_test.py`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit bc50755dcff892fecd5a0c46c4bd629742320e3c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Claude Opus 4.5 Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Nicholas Tindle Co-authored-by: Otto --- .../api/features/chat/tools/find_block.py | 158 ++++++++++++------ .../features/chat/tools/find_block_test.py | 139 +++++++++++++++ .../api/features/chat/tools/run_block.py | 17 ++ .../api/features/chat/tools/run_block_test.py | 106 ++++++++++++ .../backend/data/execution_queue_test.py | 2 - 5 files changed, 367 insertions(+), 55 deletions(-) create mode 100644 autogpt_platform/backend/backend/api/features/chat/tools/find_block_test.py create mode 100644 autogpt_platform/backend/backend/api/features/chat/tools/run_block_test.py diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/find_block.py b/autogpt_platform/backend/backend/api/features/chat/tools/find_block.py index 7ca85961f9..f55cd567e8 100644 --- a/autogpt_platform/backend/backend/api/features/chat/tools/find_block.py +++ b/autogpt_platform/backend/backend/api/features/chat/tools/find_block.py @@ -13,10 +13,32 @@ from backend.api.features.chat.tools.models import ( NoResultsResponse, ) from backend.api.features.store.hybrid_search import unified_hybrid_search -from backend.data.block import get_block +from backend.data.block import BlockType, get_block logger = logging.getLogger(__name__) +_TARGET_RESULTS = 10 +# Over-fetch to compensate for post-hoc filtering of graph-only blocks. +# 40 is 2x current removed; speed of query 10 vs 40 is minimial +_OVERFETCH_PAGE_SIZE = 40 + +# Block types that only work within graphs and cannot run standalone in CoPilot. +COPILOT_EXCLUDED_BLOCK_TYPES = { + BlockType.INPUT, # Graph interface definition - data enters via chat, not graph inputs + BlockType.OUTPUT, # Graph interface definition - data exits via chat, not graph outputs + BlockType.WEBHOOK, # Wait for external events - would hang forever in CoPilot + BlockType.WEBHOOK_MANUAL, # Same as WEBHOOK + BlockType.NOTE, # Visual annotation only - no runtime behavior + BlockType.HUMAN_IN_THE_LOOP, # Pauses for human approval - CoPilot IS human-in-the-loop + BlockType.AGENT, # AgentExecutorBlock requires execution_context - use run_agent tool +} + +# Specific block IDs excluded from CoPilot (STANDARD type but still require graph context) +COPILOT_EXCLUDED_BLOCK_IDS = { + # SmartDecisionMakerBlock - dynamically discovers downstream blocks via graph topology + "3b191d9f-356f-482d-8238-ba04b6d18381", +} + class FindBlockTool(BaseTool): """Tool for searching available blocks.""" @@ -88,7 +110,7 @@ class FindBlockTool(BaseTool): query=query, content_types=[ContentType.BLOCK], page=1, - page_size=10, + page_size=_OVERFETCH_PAGE_SIZE, ) if not results: @@ -108,60 +130,90 @@ class FindBlockTool(BaseTool): block = get_block(block_id) # Skip disabled blocks - if block and not block.disabled: - # Get input/output schemas - input_schema = {} - output_schema = {} - try: - input_schema = block.input_schema.jsonschema() - except Exception: - pass - try: - output_schema = block.output_schema.jsonschema() - except Exception: - pass + if not block or block.disabled: + continue - # Get categories from block instance - categories = [] - if hasattr(block, "categories") and block.categories: - categories = [cat.value for cat in block.categories] + # Skip blocks excluded from CoPilot (graph-only blocks) + if ( + block.block_type in COPILOT_EXCLUDED_BLOCK_TYPES + or block.id in COPILOT_EXCLUDED_BLOCK_IDS + ): + continue - # Extract required inputs for easier use - required_inputs: list[BlockInputFieldInfo] = [] - if input_schema: - properties = input_schema.get("properties", {}) - required_fields = set(input_schema.get("required", [])) - # Get credential field names to exclude from required inputs - credentials_fields = set( - block.input_schema.get_credentials_fields().keys() - ) - - for field_name, field_schema in properties.items(): - # Skip credential fields - they're handled separately - if field_name in credentials_fields: - continue - - required_inputs.append( - BlockInputFieldInfo( - name=field_name, - type=field_schema.get("type", "string"), - description=field_schema.get("description", ""), - required=field_name in required_fields, - default=field_schema.get("default"), - ) - ) - - blocks.append( - BlockInfoSummary( - id=block_id, - name=block.name, - description=block.description or "", - categories=categories, - input_schema=input_schema, - output_schema=output_schema, - required_inputs=required_inputs, - ) + # Get input/output schemas + input_schema = {} + output_schema = {} + try: + input_schema = block.input_schema.jsonschema() + except Exception as e: + logger.debug( + "Failed to generate input schema for block %s: %s", + block_id, + e, ) + try: + output_schema = block.output_schema.jsonschema() + except Exception as e: + logger.debug( + "Failed to generate output schema for block %s: %s", + block_id, + e, + ) + + # Get categories from block instance + categories = [] + if hasattr(block, "categories") and block.categories: + categories = [cat.value for cat in block.categories] + + # Extract required inputs for easier use + required_inputs: list[BlockInputFieldInfo] = [] + if input_schema: + properties = input_schema.get("properties", {}) + required_fields = set(input_schema.get("required", [])) + # Get credential field names to exclude from required inputs + credentials_fields = set( + block.input_schema.get_credentials_fields().keys() + ) + + for field_name, field_schema in properties.items(): + # Skip credential fields - they're handled separately + if field_name in credentials_fields: + continue + + required_inputs.append( + BlockInputFieldInfo( + name=field_name, + type=field_schema.get("type", "string"), + description=field_schema.get("description", ""), + required=field_name in required_fields, + default=field_schema.get("default"), + ) + ) + + blocks.append( + BlockInfoSummary( + id=block_id, + name=block.name, + description=block.description or "", + categories=categories, + input_schema=input_schema, + output_schema=output_schema, + required_inputs=required_inputs, + ) + ) + + if len(blocks) >= _TARGET_RESULTS: + break + + if blocks and len(blocks) < _TARGET_RESULTS: + logger.debug( + "find_block returned %d/%d results for query '%s' " + "(filtered %d excluded/disabled blocks)", + len(blocks), + _TARGET_RESULTS, + query, + len(results) - len(blocks), + ) if not blocks: return NoResultsResponse( diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/find_block_test.py b/autogpt_platform/backend/backend/api/features/chat/tools/find_block_test.py new file mode 100644 index 0000000000..0f3d4cbfa5 --- /dev/null +++ b/autogpt_platform/backend/backend/api/features/chat/tools/find_block_test.py @@ -0,0 +1,139 @@ +"""Tests for block filtering in FindBlockTool.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from backend.api.features.chat.tools.find_block import ( + COPILOT_EXCLUDED_BLOCK_IDS, + COPILOT_EXCLUDED_BLOCK_TYPES, + FindBlockTool, +) +from backend.api.features.chat.tools.models import BlockListResponse +from backend.data.block import BlockType + +from ._test_data import make_session + +_TEST_USER_ID = "test-user-find-block" + + +def make_mock_block( + block_id: str, name: str, block_type: BlockType, disabled: bool = False +): + """Create a mock block for testing.""" + mock = MagicMock() + mock.id = block_id + mock.name = name + mock.description = f"{name} description" + mock.block_type = block_type + mock.disabled = disabled + mock.input_schema = MagicMock() + mock.input_schema.jsonschema.return_value = {"properties": {}, "required": []} + mock.input_schema.get_credentials_fields.return_value = {} + mock.output_schema = MagicMock() + mock.output_schema.jsonschema.return_value = {} + mock.categories = [] + return mock + + +class TestFindBlockFiltering: + """Tests for block filtering in FindBlockTool.""" + + def test_excluded_block_types_contains_expected_types(self): + """Verify COPILOT_EXCLUDED_BLOCK_TYPES contains all graph-only types.""" + assert BlockType.INPUT in COPILOT_EXCLUDED_BLOCK_TYPES + assert BlockType.OUTPUT in COPILOT_EXCLUDED_BLOCK_TYPES + assert BlockType.WEBHOOK in COPILOT_EXCLUDED_BLOCK_TYPES + assert BlockType.WEBHOOK_MANUAL in COPILOT_EXCLUDED_BLOCK_TYPES + assert BlockType.NOTE in COPILOT_EXCLUDED_BLOCK_TYPES + assert BlockType.HUMAN_IN_THE_LOOP in COPILOT_EXCLUDED_BLOCK_TYPES + assert BlockType.AGENT in COPILOT_EXCLUDED_BLOCK_TYPES + + def test_excluded_block_ids_contains_smart_decision_maker(self): + """Verify SmartDecisionMakerBlock is in COPILOT_EXCLUDED_BLOCK_IDS.""" + assert "3b191d9f-356f-482d-8238-ba04b6d18381" in COPILOT_EXCLUDED_BLOCK_IDS + + @pytest.mark.asyncio(loop_scope="session") + async def test_excluded_block_type_filtered_from_results(self): + """Verify blocks with excluded BlockTypes are filtered from search results.""" + session = make_session(user_id=_TEST_USER_ID) + + # Mock search returns an INPUT block (excluded) and a STANDARD block (included) + search_results = [ + {"content_id": "input-block-id", "score": 0.9}, + {"content_id": "standard-block-id", "score": 0.8}, + ] + + input_block = make_mock_block("input-block-id", "Input Block", BlockType.INPUT) + standard_block = make_mock_block( + "standard-block-id", "HTTP Request", BlockType.STANDARD + ) + + def mock_get_block(block_id): + return { + "input-block-id": input_block, + "standard-block-id": standard_block, + }.get(block_id) + + with patch( + "backend.api.features.chat.tools.find_block.unified_hybrid_search", + new_callable=AsyncMock, + return_value=(search_results, 2), + ): + with patch( + "backend.api.features.chat.tools.find_block.get_block", + side_effect=mock_get_block, + ): + tool = FindBlockTool() + response = await tool._execute( + user_id=_TEST_USER_ID, session=session, query="test" + ) + + # Should only return the standard block, not the INPUT block + assert isinstance(response, BlockListResponse) + assert len(response.blocks) == 1 + assert response.blocks[0].id == "standard-block-id" + + @pytest.mark.asyncio(loop_scope="session") + async def test_excluded_block_id_filtered_from_results(self): + """Verify SmartDecisionMakerBlock is filtered from search results.""" + session = make_session(user_id=_TEST_USER_ID) + + smart_decision_id = "3b191d9f-356f-482d-8238-ba04b6d18381" + search_results = [ + {"content_id": smart_decision_id, "score": 0.9}, + {"content_id": "normal-block-id", "score": 0.8}, + ] + + # SmartDecisionMakerBlock has STANDARD type but is excluded by ID + smart_block = make_mock_block( + smart_decision_id, "Smart Decision Maker", BlockType.STANDARD + ) + normal_block = make_mock_block( + "normal-block-id", "Normal Block", BlockType.STANDARD + ) + + def mock_get_block(block_id): + return { + smart_decision_id: smart_block, + "normal-block-id": normal_block, + }.get(block_id) + + with patch( + "backend.api.features.chat.tools.find_block.unified_hybrid_search", + new_callable=AsyncMock, + return_value=(search_results, 2), + ): + with patch( + "backend.api.features.chat.tools.find_block.get_block", + side_effect=mock_get_block, + ): + tool = FindBlockTool() + response = await tool._execute( + user_id=_TEST_USER_ID, session=session, query="decision" + ) + + # Should only return normal block, not SmartDecisionMakerBlock + assert isinstance(response, BlockListResponse) + assert len(response.blocks) == 1 + assert response.blocks[0].id == "normal-block-id" diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/run_block.py b/autogpt_platform/backend/backend/api/features/chat/tools/run_block.py index 51bb2c0575..590f81ff23 100644 --- a/autogpt_platform/backend/backend/api/features/chat/tools/run_block.py +++ b/autogpt_platform/backend/backend/api/features/chat/tools/run_block.py @@ -8,6 +8,10 @@ from typing import Any from pydantic_core import PydanticUndefined from backend.api.features.chat.model import ChatSession +from backend.api.features.chat.tools.find_block import ( + COPILOT_EXCLUDED_BLOCK_IDS, + COPILOT_EXCLUDED_BLOCK_TYPES, +) from backend.data.block import get_block from backend.data.execution import ExecutionContext from backend.data.model import CredentialsMetaInput @@ -212,6 +216,19 @@ class RunBlockTool(BaseTool): session_id=session_id, ) + # Check if block is excluded from CoPilot (graph-only blocks) + if ( + block.block_type in COPILOT_EXCLUDED_BLOCK_TYPES + or block.id in COPILOT_EXCLUDED_BLOCK_IDS + ): + return ErrorResponse( + message=( + f"Block '{block.name}' cannot be run directly in CoPilot. " + "This block is designed for use within graphs only." + ), + session_id=session_id, + ) + logger.info(f"Executing block {block.name} ({block_id}) for user {user_id}") creds_manager = IntegrationCredentialsManager() diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/run_block_test.py b/autogpt_platform/backend/backend/api/features/chat/tools/run_block_test.py new file mode 100644 index 0000000000..2aae45e875 --- /dev/null +++ b/autogpt_platform/backend/backend/api/features/chat/tools/run_block_test.py @@ -0,0 +1,106 @@ +"""Tests for block execution guards in RunBlockTool.""" + +from unittest.mock import MagicMock, patch + +import pytest + +from backend.api.features.chat.tools.models import ErrorResponse +from backend.api.features.chat.tools.run_block import RunBlockTool +from backend.data.block import BlockType + +from ._test_data import make_session + +_TEST_USER_ID = "test-user-run-block" + + +def make_mock_block( + block_id: str, name: str, block_type: BlockType, disabled: bool = False +): + """Create a mock block for testing.""" + mock = MagicMock() + mock.id = block_id + mock.name = name + mock.block_type = block_type + mock.disabled = disabled + mock.input_schema = MagicMock() + mock.input_schema.jsonschema.return_value = {"properties": {}, "required": []} + mock.input_schema.get_credentials_fields_info.return_value = [] + return mock + + +class TestRunBlockFiltering: + """Tests for block execution guards in RunBlockTool.""" + + @pytest.mark.asyncio(loop_scope="session") + async def test_excluded_block_type_returns_error(self): + """Attempting to execute a block with excluded BlockType returns error.""" + session = make_session(user_id=_TEST_USER_ID) + + input_block = make_mock_block("input-block-id", "Input Block", BlockType.INPUT) + + with patch( + "backend.api.features.chat.tools.run_block.get_block", + return_value=input_block, + ): + tool = RunBlockTool() + response = await tool._execute( + user_id=_TEST_USER_ID, + session=session, + block_id="input-block-id", + input_data={}, + ) + + assert isinstance(response, ErrorResponse) + assert "cannot be run directly in CoPilot" in response.message + assert "designed for use within graphs only" in response.message + + @pytest.mark.asyncio(loop_scope="session") + async def test_excluded_block_id_returns_error(self): + """Attempting to execute SmartDecisionMakerBlock returns error.""" + session = make_session(user_id=_TEST_USER_ID) + + smart_decision_id = "3b191d9f-356f-482d-8238-ba04b6d18381" + smart_block = make_mock_block( + smart_decision_id, "Smart Decision Maker", BlockType.STANDARD + ) + + with patch( + "backend.api.features.chat.tools.run_block.get_block", + return_value=smart_block, + ): + tool = RunBlockTool() + response = await tool._execute( + user_id=_TEST_USER_ID, + session=session, + block_id=smart_decision_id, + input_data={}, + ) + + assert isinstance(response, ErrorResponse) + assert "cannot be run directly in CoPilot" in response.message + + @pytest.mark.asyncio(loop_scope="session") + async def test_non_excluded_block_passes_guard(self): + """Non-excluded blocks pass the filtering guard (may fail later for other reasons).""" + session = make_session(user_id=_TEST_USER_ID) + + standard_block = make_mock_block( + "standard-id", "HTTP Request", BlockType.STANDARD + ) + + with patch( + "backend.api.features.chat.tools.run_block.get_block", + return_value=standard_block, + ): + tool = RunBlockTool() + response = await tool._execute( + user_id=_TEST_USER_ID, + session=session, + block_id="standard-id", + input_data={}, + ) + + # Should NOT be an ErrorResponse about CoPilot exclusion + # (may be other errors like missing credentials, but not the exclusion guard) + if isinstance(response, ErrorResponse): + assert "cannot be run directly in CoPilot" not in response.message diff --git a/autogpt_platform/backend/backend/data/execution_queue_test.py b/autogpt_platform/backend/backend/data/execution_queue_test.py index ffe0fb265b..7a76adfe05 100644 --- a/autogpt_platform/backend/backend/data/execution_queue_test.py +++ b/autogpt_platform/backend/backend/data/execution_queue_test.py @@ -3,8 +3,6 @@ import queue import threading -import pytest - from backend.data.execution import ExecutionQueue