Skip to content

fix(backend): prevent UnicodeEncodeError for emoji in ExecuteCodeBlock - #1

Closed
Iitian001 wants to merge 79 commits into
devfrom
main
Closed

Iitian001 wants to merge 79 commits into
devfrom
main

Conversation

@Iitian001

Copy link
Copy Markdown
Owner

Closes Significant-Gravitas#13551. Fix: use ensure_ascii=True in json.dumps() when serializing variables into AGPT_VARIABLES env var so emoji/surrogate pairs are safely escaped to \uXXXX sequences. Added test_emoji_in_variables_is_safe_for_env_var covering emoji and non-ASCII chars.

ntindle and others added 30 commits July 22, 2026 15:26
…Gravitas#13666)

### Why / What / How

The Library could only be sorted by creation date or last-modified date, so an agent you just ran could be buried far down the list with no way to surface it. Users want to quickly find the agents they've been running.

This PR adds a **"Last Run"** sort option to the Library that orders agents by the timestamp of their most recent execution.

**How:**
- A new nullable `lastRunAt` column on `LibraryAgent` records when each agent was last run.
- `create_graph_execution` stamps `lastRunAt = now()` on the user's matching `LibraryAgent`(s) whenever a run is created. This is best-effort: a failure is logged and does not block execution.
- The Library listing can be sorted by `lastRunAt` (**"Last Run"**)

Closes Significant-Gravitas#9860

### Changes 🏗️

**Backend**
- `schema.prisma`: add nullable `LibraryAgent.lastRunAt`, with migration `20260723120000_add_library_agent_last_run_at`.
- `data/execution.py`: update `LibraryAgent.lastRunAt` on graph-execution creation (best-effort, non-blocking).
- `api/features/library/model.py`: expose `last_run_at` on the `LibraryAgent` model and add `LAST_RUN` to `LibraryAgentSort`.
- `api/features/library/db.py`: order by `lastRunAt` descending when sorting by last run.

**Frontend**
- `LibrarySortMenu`: add the "Last Run" option and its label.
- Regenerated `openapi.json` (generated API client follows at build time).

### 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:
  - [x] Run an agent, open the Library, sort by "Last Run" — the just-run agent appears at the top
  - [x] Run a second agent and confirm it moves above the first
  - [x] Confirm "Creation Date" and "Last Modified" sorts are unaffected
  - [x] Confirm agents that have never been run still appear in the list

---

Supersedes and builds on the earlier implementations in Significant-Gravitas#11891 and Significant-Gravitas#13351.

Co-authored-by: @Medyan-Naser <73041442+Medyan-Naser@users.noreply.github.com>
Co-authored-by: @jashwanth-reddy-g <289575991+jashwanth-reddy-g@users.noreply.github.com>
Co-authored-by: Reinier van der Leer <pwuts@agpt.co>
…smatch (Significant-Gravitas#13559)

### Why / What / How

**Why.** The webhook ingress route (`webhook_ingress_generic`) picks the
`WebhooksManager` from the `{provider}` in the URL path, then loads the
stored webhook row by ID — but never checks that the two agree. Because
the manager is chosen from the (untrusted) path, a webhook registered
under one provider can be processed by a *different* provider's manager.
Managers for providers whose protocol has no signing scheme (Compass,
Slant3D) inherit the no-op `verify_signature` from
`BaseWebhooksManager`, so routing a webhook's UUID through one of those
paths skips the verification the webhook's real provider would enforce.

Concretely: a generic webhook configured with a `secret_token` correctly
rejects a request missing `X-Webhook-Secret` on its own
`/generic_webhook/...` path, but the same UUID sent to
`/compass/webhooks/{id}/ingress` runs Compass's no-op verifier instead
of the generic manager's secret check, and the attached graph executes.

This is a gap from when signature verification was consolidated into a
separate, provider-overridable `verify_signature` step (Significant-Gravitas#13135): the
step keys off the URL provider rather than the stored webhook's
provider.

**What.** Reject requests whose URL provider doesn't match the stored
`webhook.provider`, before verification and payload validation.

**How.** After the webhook row loads, compare `webhook.provider` to the
path `provider`. On mismatch, return **403** (consistent with the
existing "don't 404 — that leaks webhook existence" handling) and log
the mismatch. No change to any valid-path behavior.

### Changes 🏗️

| File | Change |
|---|---|
| `api/features/integrations/router.py` | Guard
`webhook_ingress_generic`: 403 when `webhook.provider != provider`,
before `verify_signature` / `validate_payload` |
| `api/features/integrations/webhook_ingress_test.py` | Add
`TestProviderPathConfusion`: a secret-protected generic webhook can't be
triggered via `/compass/...` or `/slant3d/...`; matching-provider path
still processes |

### 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:
- [x] `poetry run pytest
backend/api/features/integrations/webhook_ingress_test.py` — 27 passed
- [x] Verified the two new cross-provider tests fail with the guard
removed and pass with it in place
  - [x] `ruff` / `black` / `isort` / `pyright` clean on changed files

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This PR fixes 1 broken outbound documentation link:
- `README.md`: `https://polyformproject.org/licenses/shield/1.0.0`
(removed breaking trailing slash)

---------

Co-authored-by: mirkosalvato1-ctrl <227404546+mirkosalvato1-ctrl@users.noreply.github.com>
Co-authored-by: Reinier van der Leer <github@pwuts.nl>
…its don't drop it (Significant-Gravitas#13441)

### Why / What / How

**Why** — When AutoPilot edits an agent, the agent-graph tools
frequently receive no usable `agent_json` (OPEN-3188): the tools
declared it as a bare `{"type": "object"}`, and constrained
tool-argument decoders (SDK/OpenRouter path) commonly collapse an
unstructured object to `{}` or drop it — worst exactly when editing a
large existing agent.

**What**
- Structured `AGENT_JSON_SCHEMA` (typed `["object", "string"]`,
`nodes`/`links`/… properties) so decoders have keys to follow instead of
collapsing the graph.
- `agent_json_ref` param on all five agent-graph tools: point at the
workspace `agent.json` instead of re-emitting the graph inline.
- `write_graph_to` param on `find_library_agent` (review request):
writes the agent's graph server-side to a workspace file and returns an
`@@agptfile` ref for `edit_agent`, so existing graphs never transit
context.

**How** — A shared `agent_json_input` module resolves all input forms
server-side (inline object, stringified JSON, `@@agptfile:` token,
`workspace://` path) with sanitized errors, on both the SDK and baseline
paths. Reconciled with Significant-Gravitas#13579 after it merged: its
`"@@agptfile:<path>"`-string contract is kept and now also works on the
baseline path; its `coerce_agent_json` is subsumed by the shared
resolver; its `fix_agent_graph` `write_to`/`fix_diff` are preserved
(write helper now shared). Also fixes a regression where the structured
schema made SDK file-ref expansion recurse into inline graphs and expand
tokens inside `name`/`description`.

Deferred (per ticket): diff/patch-style `edit_agent`.

### Changes 🏗️

- **New** `copilot/tools/agent_json_input.py` — shared schema, input
resolver, workspace-write helper
- `create_agent` / `edit_agent` / `customize_agent` /
`validate_agent_graph` / `fix_agent_graph` — structured schema +
`agent_json_ref`
- `find_library_agent` — `write_graph_to` file-output mode
- `sdk/file_ref.py` — union `object/string` slots skip dict recursion
- `sdk/agent_generation_guide.md` — editing flow leads with
`write_graph_to`; single canonical ref spelling
- Tests: resolver suite (15 cases) + ref/write coverage across the tool
suites; schema char budget re-measured 47800→51500

### 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:
- [x] `agent_json_input_test.py`, `edit_agent_test.py`,
`find_library_agent_test.py`, `fix_agent_test.py`,
`validate_agent_test.py`, `tool_schema_test.py`, `file_ref_test.py` —
all green locally and in CI
  - [x] `poetry run format` / `lint` clean

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…icant-Gravitas#13645)

### Why / What / How

**Why:** Users sharing a screenshot with CoPilot currently have to save
it to disk, click the "+" button, and pick the file. Pasting an image
straight from the clipboard (Ctrl/Cmd+V) — standard UX in ChatGPT,
Claude, Slack, Discord — did nothing. Fixes OPEN-3151.

**What:** Pasting into the CoPilot chat input now attaches clipboard
files (screenshots, copied images, files copied from the OS) to the
message being composed, reusing the exact same local-attachment flow as
the "+" menu and drag-and-drop. Plain-text paste is untouched.

**How:** A `handlePaste` handler on the composer textarea reads
`ClipboardEvent.clipboardData.files`. When files are present it prevents
the default paste and appends them to the existing `attachments` state
(`kind: "local"`), so upload, validation (max 10 files / 100MB in
`useSendMessage`), chips display, and multimodal handling all work
unchanged. Generic clipboard screenshot names (`image.png`) are renamed
to timestamped `pasted-image-…` names so multiple pastes stay
distinguishable. Paste-attach is ignored while the composer is busy
(disabled/streaming/uploading), matching the disabled "+" menu.

### Changes 🏗️

- `ChatInput.tsx`: add `handlePaste` wired to `PromptInputTextarea`'s
`onPaste`; attaches clipboard files via the existing
`handleFilesSelected` path
- `helpers.ts`: add `getFilesFromClipboard` — extracts files from the
clipboard and renames generic `image.*` names to unique
`pasted-image-<timestamp>` names
- `ChatInput.test.tsx`: 7 new integration tests covering paste-attach +
send, attachment-only send, name preservation for non-generic files,
distinct names for multiple generic images, default-paste prevention,
plain-text paste passthrough, and no-attach while uploading

### 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:
- [x] `pnpm vitest run src/app/(platform)/copilot` — all 106 files /
1352 tests pass, including 7 new clipboard-paste tests
  - [x] `prettier --check`, `next lint`, `tsc --noEmit` all clean
- [ ] Manual verification on preview: copy a screenshot, focus the
CoPilot input, press Ctrl/Cmd+V → image attaches as a chip and sends

<details>
  <summary>Example test plan</summary>

  - [ ] Create from scratch and execute an agent with at least 3 blocks
- [ ] Import an agent from file upload, and confirm it executes
correctly
  - [ ] Upload agent to marketplace
- [ ] Import an agent from marketplace and confirm it executes correctly
  - [ ] Edit an agent from monitor, and confirm it executes correctly
</details>

#### For configuration changes:

- [ ] `.env.default` is updated or already compatible with my changes
- [ ] `docker-compose.yml` is updated or already compatible with my
changes
- [ ] I have included a list of my configuration changes in the PR
description (under **Changes**)

<details>
  <summary>Examples of configuration changes</summary>

  - Changing ports
  - Adding new services that need to communicate with each other
  - Secrets or environment variable changes
  - New or infrastructure changes such as databases
</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_019Vb399jnB1hWqSWPi6UPCc

---
_Generated by [Claude
Code](https://claude.ai/code/session_019Vb399jnB1hWqSWPi6UPCc)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
…3675)

## Summary
- Adds a visually hidden description to the search command dialog
content.
- Adds focused regression coverage that verifies the dialog exposes an
aria-describedby target.

Fixes Significant-Gravitas#11035

## Testing
- Focused Vitest run for `SearchCommandModal.accessibility.test.tsx`
passed with a local no-setup config.
- `node_modules/.bin/prettier.cmd --check
src/components/organisms/SearchCommandModal/SearchCommandModal.tsx
src/components/organisms/SearchCommandModal/__tests__/SearchCommandModal.accessibility.test.tsx
src/components/organisms/SearchCommandModal/__tests__/SearchCommandModal.test.tsx`

## Notes
- `node_modules/.bin/tsc.cmd --noEmit --pretty false` is currently
blocked locally by missing generated API modules under
`src/app/api/__generated__`, also present outside this change.

Co-authored-by: zq <zhouquan1511@163.com>
…ificant-Gravitas#13677)

### Why / What / How

<!-- Why: Why does this PR exist? What problem does it solve, or what's
broken/missing without it? -->
**Why:** This PR addresses an issue where accessing a nonexistent
creator page (e.g., `/marketplace/creator/nonexistent-slug`) would cause
an unhandled `ApiError: Not Found` during Next.js metadata generation.
Previously, the `generateMetadata` function in
`autogpt_platform/frontend/src/app/(platform)/marketplace/creator/[creator]/page.tsx`
called `getV2GetCreatorDetails` without error handling. When the backend
returned a 404 for a missing creator, `customMutator` would throw an
`ApiError`, which propagated as an unhandled exception, leading to
errors in production logs (primarily from bots/crawlers).

<!-- What: What does this PR change? Summarize the changes at a high
level. -->
**What:** This change introduces robust error handling within the
`generateMetadata` function for creator pages.

<!-- How: How does it work? Describe the approach, key implementation
details, or architecture decisions. -->
**How:**
1. Imported `ApiError` from `@/lib/autogpt-server-api/helpers` and
`notFound` from `next/navigation`.
2. Wrapped the `await getV2GetCreatorDetails(...)` call in a `try/catch`
block.
3. Inside the `catch` block, if the error is an `ApiError` with a
`status` of 404, `notFound()` is called. This correctly signals to
Next.js that the resource is not found, rendering the nearest
`not-found.tsx` boundary and returning a 404 HTTP status.
4. Any other type of error or `ApiError` with a non-404 status is
re-thrown, ensuring that genuine server errors or unexpected issues are
still surfaced and handled by the global error boundary or Sentry.

### Changes 🏗️

<!-- List the key changes. Keep it higher level than the diff but
specific enough to highlight what's new/modified. -->
- Implemented `try/catch` block for `getV2GetCreatorDetails` call within
`generateMetadata` function in
`autogpt_platform/frontend/src/app/(platform)/marketplace/creator/[creator]/page.tsx`.
- Added specific error handling for `ApiError` with a 404 status,
utilizing `notFound()` from `next/navigation`.
- Ensured other errors are re-thrown for proper surfacing.
- Imported necessary modules: `ApiError` and `notFound`.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [ ] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
- [ ] Navigate to a valid creator page (e.g.,
`/marketplace/creator/some-existing-creator-slug`) and verify it loads
correctly.
- [ ] Navigate to a nonexistent creator page (e.g.,
`/marketplace/creator/nonexistent-slug`) and verify that the 404 page is
displayed.
- [ ] Check browser console and server logs to ensure no unhandled
errors or exceptions are thrown when accessing a nonexistent creator
page.

<details>
  <summary>Example test plan</summary>
  
  - [ ] Create from scratch and execute an agent with at least 3 blocks
- [ ] Import an agent from file upload, and confirm it executes
correctly
  - [ ] Upload agent to marketplace
- [ ] Import an agent from marketplace and confirm it executes correctly
  - [ ] Edit an agent from monitor, and confirm it executes correctly
</details>

#### For configuration changes:

- [ ] `.env.default` is updated or already compatible with my changes
- [ ] `docker-compose.yml` is updated or already compatible with my
changes
- [ ] I have included a list of my configuration changes in the PR
description (under **Changes**)

<details>
  <summary>Examples of configuration changes</summary>

  - Changing ports
  - Adding new services that need to communicate with each other
  - Secrets or environment variable changes
  - New or infrastructure changes such as databases
</details>

Fixes
[BUILDER-733](https://significant-gravitas.sentry.io/issues/7261265514/?seerDrawer=true)
Fixes
[SENTRY-1124](https://linear.app/autogpt/issue/SENTRY-1124/apierror-not-found)

---------

Co-authored-by: Nicholas Tindle <nicholas.tindle@agpt.co>
Co-authored-by: seer-by-sentry[bot] <157164994+seer-by-sentry[bot]@users.noreply.github.com>
Co-authored-by: Lluis Agusti <hi@llu.lu>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Ubbe <hi@ubbe.dev>
Significant-Gravitas#13679)

### Why / What / How

**Why:** The block-docs generator (`generate_block_docs.py`) emitted
"Creating Your Own Blocks" guide links pointing at the legacy
`docs.agpt.co` host, which flowed into the generated
`docs/integrations/README.md`. That host still 301-redirects to
`agpt.co/docs`, but PR Significant-Gravitas#13598 already moved the copilot docs tools onto
the canonical `agpt.co/docs` host; this generator (and a few stray
manual docs) were left behind, so the links took an extra redirect hop
and weren't single-sourced. Fixes
[OPEN-3209](https://linear.app/autogpt/issue/OPEN-3209).

**What:** Repoint the generator's guide links to the canonical
`agpt.co/docs` host, regenerate the overview, and repoint the stray
`docs.agpt.co` references in a few manual docs files.

**How:** Build the two guide URLs via `backend.util.docs.make_doc_url`
(the single-source helper Significant-Gravitas#13598 added) instead of hardcoding them, so
the host and URL shape can't drift from the copilot docs tools. The
helper strips the `.md` extension and canonicalizes `_`→`-`, producing
`https://agpt.co/docs/platform/new-blocks` and `.../block-sdk-guide`,
both verified to render live.

**Note on the ticket's second gap (site-recategorized pages 404ing):**
verified no longer reproducing. Both synthesized URLs now render
correctly on the live site via GitBook redirects added since the ticket
was filed (`agpt.co/docs/platform/new-blocks` → og:title "Build your own
Blocks"), so no sitemap-fetch or redirect machinery is needed here.

### Changes 🏗️

- `scripts/generate_block_docs.py`: build the overview's guide links via
`make_doc_url` (module-level import) instead of hardcoded `docs.agpt.co`
URLs.
- `docs/integrations/README.md`: regenerated — the two guide links now
point at `agpt.co/docs`.
- `scripts/test_generate_block_docs.py`: add `TestOverviewGuideLinks`
(fails without the fix) asserting the overview never emits
`docs.agpt.co` and uses the `agpt.co/docs` host.
- Repointed the stray `docs.agpt.co` references in these manual files:
`docs/content/index.md`, `docs/platform/aimlapi.md`,
`docs/platform/gemini.md`,
`autogpt_platform/backend/load-tests/README.md`. (Other `docs.agpt.co`
references elsewhere in the repo — e.g. root `README.md`, `classic/*`,
frontend styleguide — are outside this ticket's scope and left
untouched.)

### 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:
- [x] `poetry run pytest scripts/test_generate_block_docs.py` — 30
passed
- [x] Verified `TestOverviewGuideLinks` fails on the pre-fix generator
and passes after
- [x] `poetry run python scripts/generate_block_docs.py --check` — "All
documentation is in sync!"
- [x] Confirmed the new URLs render live
(`agpt.co/docs/platform/new-blocks`, `.../block-sdk-guide`,
`.../getting-started`, `/platform`, `/docs`)
…f dropping memory ops (Significant-Gravitas#13686)

### Why / What / How

**Why.** Sentry issue
[SENTRY-1384](https://linear.app/autogpt/issue/SENTRY-1384) — `Error
executing FalkorDB query: Max pending queries exceeded`. FalkorDB (the
Graphiti-backed CoPilot memory store) is a single shared server for
every user's graph. It runs queries on a bounded worker pool and parks
the overflow in a pending-query queue with a finite cap. Under
concurrent memory traffic — warm-context recall on every chat turn,
ingestion `add_episode` fan-out, and community rebuilds, none of which
are globally rate-limited — the combined arrival rate can exceed what
the pool drains plus what the queue holds, and FalkorDB rejects the
surplus with this message.

The rejection is **transient and self-clearing** (the queue drains in
milliseconds) and a **pre-execution** rejection (the query never runs).
But today two bad things happen: the memory op is silently dropped
(warm-context returns empty / an episode fails to store), and
graphiti-core's `execute_query` logs an `ERROR` that Sentry captures —
recurring noise for a condition that isn't a bug.

**What.** `AutoGPTFalkorDriver.execute_query` now retries this specific
backpressure with bounded, jittered exponential backoff, so a load spike
degrades into a slightly slower memory op instead of a dropped one plus
a Sentry alert. Only a genuinely exhausted retry budget surfaces the
error.

**How.**
- Retry is **scoped strictly** to the `max pending queries exceeded`
signature. Because it's a pre-execution rejection, retrying is
side-effect-free even for writes. Every other error (Cypher typo,
missing graph, connection teardown) still fails fast on the first
attempt, exactly as upstream.
- **Bounded + jittered exponential backoff**
(`GRAPHITI_FALKORDB_QUERY_MAX_ATTEMPTS`, default 3;
`GRAPHITI_FALKORDB_QUERY_BACKOFF_BASE`, default 0.1s). The backoff
drains the queue *between* attempts rather than amplifying the pileup,
and jitter prevents concurrent callers from retrying in lockstep. Total
backoff fits inside the existing 8s warm-context timeout.
- **Sentry-on-exhaustion only.** The override reimplements the query
body (rather than delegating to `super()`) so intermediate retries emit
no `ERROR` — only the terminal failure logs, under the same
`graphiti_core.driver.falkordb_driver` logger name so Sentry grouping
and the SENTRY-1387 benign-teardown filter keep working.

### Changes 🏗️

- `falkordb_driver.py`: `execute_query` override with scoped, bounded,
jittered-backoff retry on pending-queue overflow;
`_pending_queue_retry_delay` + `_to_records` helpers. Result-shaping
mirrors upstream; typed so downstream consumers keep their inference.
- `config.py`: `falkordb_query_max_attempts` (3) and
`falkordb_query_backoff_base` (0.1s) tunables under the `GRAPHITI_` env
prefix.
- `falkordb_driver_test.py`: 5 tests — success passthrough,
retry-then-succeed, exhaust-then-raise-with-single-terminal-log,
non-overflow-fails-fast, and the `already indexed` short-circuit.

### 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:
- [x] `poetry run pytest
backend/copilot/graphiti/falkordb_driver_test.py` — all pass; the retry
test fails without the fix (upstream raises on the first overflow)
- [x] `poetry run format` and `poetry run lint` clean (ruff, isort,
black, pyright)
- [x] Package-level pyright green — verified the new return annotation
preserves downstream inference in `migrations/backfill_edge_props.py`

#### For configuration changes:
- [x] `.env.default` is updated or already compatible with my changes
(new settings have safe defaults; no `.env` change required)
- [x] `docker-compose.yml` is updated or already compatible with my
changes (no infra change)
…entry (Significant-Gravitas#13678)

### Why / What / How

**Why:** Sentry issue
[SENTRY-1387](https://linear.app/autogpt/issue/SENTRY-1387) — `Error
executing FalkorDB query: Buffer is closed.` fires repeatedly. It's a
benign connection-teardown race in the CoPilot Graphiti memory path, not
a real failure: `graphiti-core`'s `FalkorDriver.execute_query` logs at
`ERROR` and re-raises whenever a query races the closing of its
`redis.asyncio` connection — either the fire-and-forget cache-eviction
close (`client.py` `_close_client_driver` →
`loop.create_task(driver.close())`) or a per-request `driver.close()` in
a `finally` block. CoPilot memory callers swallow the exception and chat
degrades gracefully, so the only impact is Sentry noise that buries real
errors. The existing `build_indices=False` mitigation only removes one
source of the race; other teardown paths (eviction, deploy, shutdown)
still trip it.

**What:** Extend the Sentry `_before_send` filter to drop the two benign
FalkorDB teardown signatures — `"Buffer is closed"` and `"Connection
closed by server"` — scoped to the
`graphiti_core.driver.falkordb_driver` logger. Genuine query errors
(Cypher typos, missing graphs) from the same logger still reach Sentry.

**How:** Mirrors the existing pika reconnect-noise filter already in
`metrics.py`:
- Log-based events (the dominant signal, captured via
`LoggingIntegration`): dropped when `logger ==
graphiti_core.driver.falkordb_driver` and the message contains a
teardown signature.
- `exc_info` events (a caller that doesn't swallow the re-raised error):
dropped when the exception message contains a teardown signature **and**
the exception type originates from a `falkordb`/`redis` module — so the
scope stays tight.

### Changes 🏗️

- `backend/util/metrics.py`: add `_FALKORDB_DRIVER_LOGGER`,
`_FALKORDB_TEARDOWN_SIGNATURES`, `_FALKORDB_MODULE_INDICATORS`; add the
log-based and `exc_info` drop branches in `_before_send`.
- `backend/util/metrics_test.py`: 6 new tests — both teardown messages
dropped, the `exc_info` redis path dropped, a genuine query error kept,
the same string from a non-graphiti logger kept, and a
signature-coverage sanity check.

### 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:
- [x] `poetry run pytest backend/util/metrics_test.py` — 14 passed (7
pre-existing + 7 new)
- [x] Verified before the fix that a
`graphiti_core.driver.falkordb_driver` "Buffer is closed" event is
**not** dropped (reaches Sentry), and after the fix that it **is**
dropped
- [x] Confirmed a genuine (non-teardown) FalkorDB query error from the
same logger still passes through the filter
  - [x] `poetry run format` and `poetry run lint` clean
…the full set (Significant-Gravitas#13681)

### Why / What / How

**Why** — Resolves
[OPEN-3212](https://linear.app/autogpt/issue/OPEN-3212) / Significant-Gravitas#13633. On the
library agent view's **Triggers** tab, the router decides whether a
selected item is a *trigger agent* or a *webhook-trigger preset* by
checking membership in the agent's presets list. That list was fetched
as a single page capped at `PRESETS_PAGE_SIZE = 100`. For an agent with
more than 100 presets, `presetsComplete` stayed `false`, so an
unknown/stale selection fell back to a throwaway by-ID preset fetch (a
graceful not-found card, but one wasted request per unknown selection)
and lost the "resolve type before mount" guarantee. The sidebar
trigger/template counts were also capped at the first 100.

**What** — `useAgentPresetsQuery` now pages through *all* of an agent's
presets so membership routing is authoritative, instead of only reading
the first 100. No backend or API-contract change (the shared `/presets`
endpoint, its copilot RPC, and the external paginated API are
untouched).

**How**
- Switched `useAgentPresetsQuery` from the single-page
`useGetV2ListPresets` to the generated `useGetV2ListPresetsInfinite`,
and eagerly fetch each next page (via a `useEffect` on `hasNextPage`) up
to a safety cap of `MAX_PRESET_PAGES = 20` (2000 presets/agent).
- The hook flattens the pages into one `presets` list (`unpaginate`) and
exposes:
- `presetsComplete` — every page has loaded, so membership is
authoritative.
- `presetsSettled` — no more pages will be fetched (complete **or**
stopped at the cap).
- The detail-pane router now treats `presetsSettled` as `listsResolved`,
so while pagination is still in flight it shows the loading state rather
than prematurely resolving a selection against a partial list. The
graceful by-ID fallback (`deriveSelectedTriggerKind` → `webhook-trigger`
when `!presetsComplete`) is preserved only for the beyond-cap edge case.
- Common case (≤100 presets) is unchanged: one request,
`presetsComplete` true immediately.

### Changes 🏗️

- `NewAgentLibraryView/hooks/useAgentPresetsQuery.ts` — eager full
pagination via the infinite presets query; expose `presets` /
`presetsComplete` / `presetsSettled`.
- `NewAgentLibraryView/useNewAgentLibraryView.ts` — consume the new
shape; gate `listsResolved` on `presetsSettled`.
- `NewAgentLibraryView/.../SidebarRunsList/useSidebarRunsList.ts` — read
the flattened `presets` (fixes capped trigger/template counts).
- `agents/[id]/__tests__/trigger-agents.test.tsx` — two multi-page
tests: a preset on a later page resolves from membership, and an unknown
ID across a fully-paginated list lands on not-found **without** a by-ID
fetch. Repurposed the former single-page "incomplete page" test.

### 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:
- [x] `pnpm exec vitest run` on `trigger-agents.test.tsx` — 19 passed (2
new tests fail without the fix, pass with it)
  - [x] `pnpm format`, `pnpm lint`, `pnpm types` all clean
- [x] Agent with ≤100 presets: Triggers tab resolves selections with a
single presets request (unchanged behavior)
- [x] Agent with >100 presets: all trigger/template rows and counts
show; a webhook trigger beyond the first 100 opens its detail view; a
stale/deleted trigger ID shows the not-found card without a wasted by-ID
request

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ignificant-Gravitas#13684)

### Why / What / How

**Why:** When a user uploads an image or logo that is too large (e.g. an
agent logo), the server returns HTTP 413 and the UI shows only a generic
error — it never tells the user the file is too large or what the size
limit is. Root cause: image/logo uploads went through the Next.js proxy,
which rejects request bodies over ~4.5MB (Vercel serverless limit) with
a **413 before the request ever reaches the backend**. That 413 has no
backend JSON body, so the error message collapsed to a bare `HTTP 413`.
Meanwhile the real backend limits are much higher (store media 50MB,
OAuth logo 3MB), so files the backend would happily accept were being
rejected upstream with an unhelpful message.

Closes OPEN-2844 / Significant-Gravitas#11448.

**What:** Route all image/logo uploads directly to the backend
(bypassing the proxy, the same approach PR Significant-Gravitas#12315 used for
workspace/CoPilot uploads), and add clear file-size messaging — both a
client-side pre-check and 413-aware error parsing.

**How:**
- Added `uploadSubmissionMediaDirect` and `uploadOAuthAppLogoDirect` in
`src/lib/direct-upload.ts` that `POST` straight to the backend
(`/api/store/submissions/media`, `/api/oauth/apps/{app_id}/logo/upload`)
with the auth token, bypassing `/api/proxy`. Now the real backend size
limits apply and the backend's own error messages surface.
- Added a shared `getFileSizeError(file, maxSizeMB)` helper + size
constants (`SUBMISSION_MEDIA_MAX_SIZE_MB = 50`, `OAUTH_LOGO_MAX_SIZE_MB
= 3`, mirroring the backend) so each call site rejects oversized files
*before* uploading with a clear "File is too large … Maximum size is
NMB" message.
- Added a 413-aware `readUploadError` as a safety net for any
proxy/ingress that still returns 413 with a non-JSON body.
- Rewired the four image/logo upload paths to use the new helpers.

### Changes 🏗️

- **`src/lib/direct-upload.ts`**: new `uploadSubmissionMediaDirect`,
`uploadOAuthAppLogoDirect`, `getFileSizeError`, size constants,
413-aware error parsing; refactored the existing `uploadFileDirect` onto
a shared `postFileToBackend` helper.
- **`useThumbnailImages.ts`** (marketplace/store submission agent
thumbnail — the path in the ticket): direct upload + size pre-check +
toast.
- **`useProfilePage.ts`** (modern profile avatar): direct upload + size
pre-check; `isUploading` now backed by local state.
- **`useOAuthApps.ts`** (OAuth app logo): direct upload + size
pre-check; query invalidation moved to a `finally`.
- **`__legacy__/ProfileInfoForm.tsx`** (legacy profile avatar —
previously failed **silently** with only a `console.error`): direct
upload + size pre-check + toast feedback.
- **Tests**: new `src/lib/__tests__/direct-upload.test.ts` (URL bypass,
413 message, backend `detail` message, size helper); updated
`ThumbnailImages`, settings-profile, and existing suites to mock the
direct uploader and added "file too large" pre-check tests.

### 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:
- [x] `pnpm test:unit` for the affected suites (53 tests) — green,
including new oversized-file pre-check tests that assert the "File too
large" toast and that no upload is attempted
  - [x] `pnpm types` and `pnpm lint` clean
- [ ] Manually upload a >5MB agent thumbnail in the marketplace/store
submission flow and confirm it now succeeds (was 413) up to the 50MB
backend limit
- [ ] Upload a >50MB thumbnail and confirm a clear "File is too large …
Maximum size is 50MB" message appears before any request
- [ ] Upload a >3MB OAuth app logo and confirm the size message cites
the 3MB limit

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pped (Significant-Gravitas#13682)

### Why / What / How

**Why:** Follow-up to
[OPEN-3169](https://linear.app/autogpt/issue/OPEN-3169) / Significant-Gravitas#13394, which
made webhook-preset version migration trigger-compatibility-aware. When
a user activates a new graph version whose trigger block differs from
the one a preset's webhook was registered for, the preset is
intentionally **left pinned** to its old version (so its existing
webhook keeps working) instead of being silently migrated into a broken
state. Until now the only signal that this happened was a server-side
`logger.warning` — users who swap their agent's trigger (e.g. Telegram →
GitHub, or GitHub "on PR" → "on issue") got **no UI notification** that
some presets were left behind and need reconfiguration.

**What:** Surface the skipped/incompatible presets to the user at the
moment of the swap so they know to reconfigure the trigger on the new
version.

**How:** The migration helper now reports which presets it skipped; the
graph-activation endpoints return that in their response; the builder
shows a warning toast on save.

> Note: the backend notification server (`queue_notification`) is
disabled in production, so a "send a notification" approach would be a
no-op in prod. The effective path is to return the skipped presets in
the activation response and surface them in the frontend — which is what
this PR does. No change to the correctness behavior shipped in
OPEN-3169.

### Changes 🏗️

**Backend**
- `migrate_webhook_presets_to_new_version` now returns a
`WebhookPresetMigrationResult` (migrated count + list of skipped presets
with `id`, `name`, `pinned_version`) instead of just an `int`.
- `PUT /graphs/{id}` now returns a new `UpdateGraphResponse` (`{ graph,
skipped_webhook_presets }`) — **response shape change** for this
endpoint.
- `PUT /graphs/{id}/versions/active` now returns a new
`SetActiveGraphVersionResponse` (`{ skipped_webhook_presets }`) —
previously returned `None`.
- New models `SkippedWebhookPreset` / `WebhookPresetMigrationResult` in
`library/model.py`.
- The CoPilot/AutoPilot path (`update_graph_in_library`) intentionally
discards the migration result (no user-facing channel there); documented
inline.

**Frontend**
- Builder save (`useSaveGraph`) shows an info toast listing the affected
preset name(s) with a "reconfigure the trigger" hint when activation
returns skipped presets.
- Shared helper `notifySkippedWebhookPresets` (unit-tested directly).
- Regenerated `openapi.json` (client `__generated__` is gitignored and
regenerated by CI).

**Scope note:** Frontend surfacing is wired on the builder-save path
(the ticket's core trigger-swap scenario). The version-**revert** path
(`useBuilderChatPanel`) gates on internal `revertTargetVersion` state
that isn't reachable at unit-test level, so it's intentionally left
unwired for now — but `set_graph_active_version` already returns the
skipped presets, so wiring a toast there later is a drop-in.

### 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:
- [x] Backend unit tests (`test/test_migrate_webhook_presets.py`, 16):
migration returns skipped presets; both endpoints surface them in the
response; happy path (same trigger) still migrates with no skipped
presets.
- [x] Frontend unit tests (`useSaveGraph.test.tsx`,
`skippedWebhookPresets.test.ts`): warning toast fires (singular +
plural) when presets are skipped, and does not fire when none are.
- [x] `pnpm types` / `pnpm format` / `pnpm lint`, `poetry run format` /
`lint` all clean.
- [ ] Manual: build an agent with a webhook trigger + a preset, save a
new active version that swaps the trigger block, confirm the warning
toast lists the preset and the preset stays on its old version.
Updated the token used for dispatch events from DISPATCH_TOKEN to DEPLOY_TOKEN.
…library agents (Significant-Gravitas#13680)

### Why / What / How

**Why:** When you add an agent from the marketplace to your library, it
shows up with the creator's *original* graph title and description — not
the title/description it was published under — and with no image. The
intended behaviour (OPEN-2478 / Significant-Gravitas#9879) is that a downloaded agent
appears in your library just as it does in the marketplace.

**What:** Snapshot the marketplace listing's published name, description
and image onto the `LibraryAgent` at download time, and surface them in
the library.

**How:** `LibraryAgent` already had an `imageUrl` column that was never
populated on marketplace downloads. This PR follows that same
snapshot-at-download pattern:

- Adds nullable `name` / `description` columns to `LibraryAgent`
(migration included). Null for user-created agents, which keep falling
back to the graph's own values.
- `add_graph_to_library` now reads the `StoreListingVersion` and writes
its `name`, `description` and first `imageUrls` entry onto the
`LibraryAgent` — on both the create and the restore-existing
(soft-deleted) paths, so re-downloading also refreshes the snapshot.
- `LibraryAgent.from_db` prefers the snapshotted `name`/`description`,
falling back to `graph.name`/`graph.description`.

This is scoped to the "Add to Library" download path; user-created
agents (via `create_library_agent`) are unaffected.

### Changes 🏗️

- `schema.prisma`: add `name` / `description` (both `String?`) to
`LibraryAgent`; new migration
`20260727120000_add_library_agent_marketplace_metadata`.
- `_add_to_library.py`: new `_get_marketplace_metadata()` helper;
`add_graph_to_library` snapshots name/description/imageUrl on create and
update.
- `library/model.py`: `from_db` uses `agent.name or graph.name` and
`agent.description or graph.description`.
- Tests: `_add_to_library_test.py` (snapshot written on create +
restore; helper edge cases) and `model_test.py` (override precedence +
fallback).

### 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:
- [x] `poetry run pytest
backend/api/features/library/_add_to_library_test.py
backend/api/features/library/model_test.py` — 9 passed
- [x] Verified the `from_db` fallback path keeps user-created agents on
their graph name/description
- [ ] Publish an agent to the marketplace under a different
title/description + image, add it to a fresh account's library, confirm
the library card shows the published title/description/image
…nificant-Gravitas#13685)

### Why / What / How

**Why:** First slice of the "Hire Experts" MVP (Hire-or-Raise product
direction): users will hire curated Experts from the marketplace, see
them on a Team page, and chat with them in per-expert CoPilot threads
that run installed Workflows. This PR lays the backend foundation — the
`Expert` entity, the hire/install API, and the dev roster — so the
copilot and frontend slices can build on it. Ships dark; nothing
user-facing changes.

**What:**
- New first-class `Expert` Prisma model (one table for roster templates
and hired instances) + `ExpertWorkflow` join to
`LibraryAgent`/`StoreListingVersion`, and a nullable
`ChatSession.expertId` column. Migration is purely additive.
- New feature module `backend/api/features/experts/`: data layer
(`experts_db.py`), Pydantic models, REST routes mounted at
`/api/experts`, and a dev roster seed script (Maria/Marketing,
Max/Sales, Frankie/Ops).

**How:**
- Hire resolves each template preload through the existing
`add_store_agent_to_library` seam, producing the hiring user's
`LibraryAgent` plus an `ExpertWorkflow` instance row. Hire is idempotent
per (user, template) — enforced by `@@unique([ownerUserId,
sourceTemplateId])` and a find-first in `hire_expert`. Duplicate
workflow install returns the existing row.
- Partial hire is honest: a failed preload install is caught per-item
and reported in `HireResult.failed_preloads`; the hire itself succeeds.
- All non-template queries are scoped by `ownerUserId` (+ tenancy trio
columns carried for org-readiness); templates are never owned and never
chat targets. Auth via `Security(autogpt_auth_lib.get_user_id)` like the
library routes.
- Endpoints (operation IDs drive the frontend Orval hooks in the
follow-up PR): `GET /api/experts/templates`, `POST /api/experts` (hire),
`GET /api/experts`, `GET /api/experts/{id}`, `POST
/api/experts/{id}/workflows`, `DELETE /api/experts/{id}` (archive →
204).

### Changes 🏗️

- `backend/schema.prisma`: `Expert`, `ExpertWorkflow` models;
`ChatSession.expertId` (nullable); back-relations on `User`, `Team`,
`LibraryAgent`, `StoreListingVersion`
- `backend/migrations/20260727070306_add_expert_entity/` — additive only
(2 CREATE TABLE, 1 ALTER TABLE ADD COLUMN, indexes + FKs)
- `backend/backend/api/features/experts/models.py` — `Expert`,
`ExpertWorkflowRef`, `HireResult`
- `backend/backend/api/features/experts/experts_db.py` —
`list_templates`, `list_experts`, `get_expert`, `hire_expert`
(idempotent), `install_workflow` (duplicate-safe), `archive_expert`
- `backend/backend/api/features/experts/routes.py` + mount in
`backend/backend/api/rest_api.py`
- `backend/backend/api/features/experts/seed.py` — dev roster upsert by
template name; preloads resolved from real dev marketplace listing
slugs; missing listings logged and skipped, never fatal
- Tests: `experts_db_test.py` (5, real-DB), `routes_test.py` (10, mocked
db + snapshot)

Note on user-ID scoping: this PR touches
`api/features/experts/experts_db.py` (a data-layer module); every
non-template query is scoped by `ownerUserId`, hire/install/archive
validate ownership, and cross-user access returns None/404 (covered by
tests).

### 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:
- [x] `poetry run pytest backend/api/features/experts/experts_db_test.py
-xvs` — 5 passed (idempotent hire, preload install, owner scoping,
duplicate install, seed round-trip)
- [x] `poetry run pytest backend/api/features/experts/routes_test.py
-xvs` — 10 passed (all endpoints, 404 paths, snapshot)
- [ ] Manual: run `poetry run python -m
backend.api.features.experts.seed` against dev DB and hire via API
(deferred to PR 2/3 integration)

#### 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**)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ignificant-Gravitas#13687)

### Why / What / How

**Why:** Second slice of the "Hire Experts" MVP (stacked on Significant-Gravitas#13685).
Hired Experts need their own CoPilot threads: sessions must bind to an
expert, and the copilot must speak in the expert's persona and know
which Workflows are installed on it. Plain Autopilot sessions
additionally get team awareness so Autopilot can *suggest* the right
expert with a handoff — never auto-route.

**What:**
- `POST /api/chat/sessions` accepts optional `expert_id` (validated:
owned, non-template, non-archived → else 404); `GET
/api/chat/sessions?expert_id=...` filters; session responses carry
`expert_id`.
- New `backend/copilot/expert_context.py`:
`build_expert_context(user_id, expert_id)` renders an
`<expert_identity>` + `<expert_workflows>` block for expert sessions, or
a `<team_context>` block (the user's hired experts) for plain sessions.
- Wired into the existing first-message injection
(`inject_user_context`) — one call site, plus the two stream callers
passing `session.expert_id`.

**How:**
- No engine changes. The `_CACHEABLE_SYSTEM_PROMPT` stays byte-identical
(locked by a SHA-256 assertion test); all personalization rides the
existing dynamic first-message path, so Anthropic prompt caching is
unaffected. The expert block is prepended on the dynamic side of the
cache breakpoint.
- `expert_id` flows: `CreateSessionRequest` → `create_chat_session` →
Prisma `ChatSession.expertId` (column added in Significant-Gravitas#13685) →
`ChatSessionInfo.from_db`, so every loaded session exposes it with no
extra fetch.
- Failure behavior per spec: archived/missing expert at stream time →
context block omitted, turn proceeds as plain Autopilot. All
`experts_db` lookups are wrapped in try/except returning `""` — chat
never hard-fails on expert lookup.
- Workflow execution reuses the existing `run_agent` tool and execution
pipeline unchanged; the injected `<expert_workflows>` block hands the
model the `library_agent_id`/`graph_id`s and the instruction to prefer
them.

### Changes 🏗️

- `backend/api/features/chat/routes.py` — `expert_id` on create/list
sessions + create-time validation via `experts_db.get_expert`;
`expert_id` on `CreateSessionResponse`/`SessionSummaryResponse`
- `backend/copilot/model.py` — `ChatSessionInfo.expert_id` (+ `from_db`
mapping), `expert_id` pass-through on `ChatSession.new` /
`create_chat_session` / `get_user_sessions`
- `backend/copilot/db.py` — persist `expertId` on create; `expertId`
filter on list/count
- `backend/copilot/expert_context.py` — new; pure string assembly, ~105
lines
- `backend/copilot/service.py` — `inject_user_context()` gains
`expert_id` kwarg and one prepend; stream callers in
`copilot/baseline/service.py` + `copilot/sdk/service.py` pass
`session.expert_id`
- Tests: 4 new chat-route tests (103 passed in file); 10 new
`expert_context_test.py` tests incl. system-prompt hash lock and
injection tests (63 passed with existing prompt-cache tests)

Stacked on Significant-Gravitas#13685 (base retargets to `dev` once that merges).

### 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:
- [x] `poetry run pytest backend/api/features/chat/routes_test.py` — 103
passed (incl. 4 new: persist expert_id, other-user 404, archived 404,
list filter)
- [x] `poetry run pytest backend/copilot/expert_context_test.py
backend/copilot/prompt_cache_test.py` — 63 passed (identity/team blocks,
degradation to "", system prompt byte-identical)
- [ ] Manual: create a session with an expert and confirm the first turn
shows persona + can run an installed workflow via `run_agent` (needs
local stack + seeded roster)

#### 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**)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…reads (Significant-Gravitas#13689)

### Why / What / How

**Why:** Third and final slice of the "Hire Experts" MVP (stacked on
Significant-Gravitas#13687, which stacks on Significant-Gravitas#13685). The backend lets users hire Experts and
chat with them; this PR ships the flag-gated surfaces that make the loop
usable end-to-end: hire from the marketplace, see your Team, chat in
per-expert CoPilot threads, and install Workflows on an expert.

**What:**
- Marketplace: "Meet the Experts" section (curated roster from `GET
/api/experts/templates`) with a profile sheet and Hire flow; hired
templates show a Hired state.
- New `/team` page: Autopilot card first (the generalist — "runs the
shop"), then hired expert cards with installed workflow names/counts and
Chat / Install workflow actions.
- CoPilot: per-expert threads — `/copilot?expertId=<id>` opens the
expert's latest thread or creates one with `expert_id`; the sidebar
groups threads under expert headers (Autopilot is the default group);
thread header and assistant messages show the expert's name/avatar.
- "Install on Expert…" action on marketplace agent detail + "Install
workflow" on Team cards, both via one `InstallWorkflowPicker` component
(two modes: pick-expert / pick-workflow) calling `POST
/api/experts/{id}/workflows`.

**How:**
- Everything sits behind `Flag.HIRE_EXPERTS` (`hire-experts`
LaunchDarkly flag; env override `NEXT_PUBLIC_FORCE_FLAG_HIRE_EXPERTS`).
Flag off → sections hidden, `/team` 404s, copilot behavior byte-for-byte
as today.
- Data fetching uses Orval-generated React Query hooks
(`useListExpertTemplates`, `useHireExpert`, `useListExperts`,
`useInstallExpertWorkflow`, …) regenerated from the stacked backend's
OpenAPI spec; hire/install mutations invalidate the `["/api/experts"]`
query key so all surfaces refresh.
- Copilot diffs are surgical: sidebar grouping is a pure partition of
the existing session list by `expert_id` (renders only when the flag is
on and a grouped session exists — flat list untouched otherwise);
identity comes from one shared `useExpertMap` hook; stream transport,
chat registry/stream store, tool cards, and reconnect logic are
untouched.
- Failure behavior: templates query error → section renders nothing
(marketplace never breaks); missing/archived expert → default header;
duplicate install → "Already installed on {name}" toast without a POST
(client pre-check; the API is also duplicate-safe).

### Changes 🏗️

- `src/services/feature-flags/use-get-flag.ts` — `Flag.HIRE_EXPERTS` +
`src/app/api/openapi.json` regenerated (experts tag, `expert_id` on chat
sessions). Generated hooks are gitignored per repo convention; run `pnpm
generate:api` after checkout.
- `marketplace/components/ExpertsSection/` (+ `ExpertProfileSheet`) and
`MainMarketplacePage` mount; `InstallOnExpertButton` in `AgentInfo`
- `team/` route: `page.tsx`, `useTeamPage.ts`, `AutopilotCard`,
`ExpertTeamCard`, `EmptyTeamState`
- `copilot/`: `useChatSession` (expert_id on create + latest-thread
adoption), `useSessionList` grouping, `ChatSidebar` grouped rendering,
`ChatMessagesContainer` thread header + assistant identity,
`useExpertMap`
- `components/molecules/InstallWorkflowPicker/` wired into marketplace
detail and Team page
- **Backend slice** (supports the surfaces above): `Expert.bio` +
`Expert.skills` columns (migrations `20260728100000_add_expert_bio`,
`20260728200000_add_expert_skills` — additive only) exposed on the
experts API; roster seed gains avatars (`/experts/*.svg`), bios, and
skills; expert lookups routed through `DatabaseManager` for the
Prisma-less copilot executor; expert identity moved from the
first-message context block to a per-session system-prompt suffix
(`build_expert_identity_suffix`) so it outranks the base persona — the
cacheable prompt prefix stays byte-identical
- Tests: page-level Vitest + RTL + MSW integration tests for all four
surfaces; global `getExpertsMock()` added to `mock-handlers.ts`

Note: the `hire-experts` flag must be created in LaunchDarkly before
enabling in any environment. Screenshots deferred — will attach from a
running stack before undrafting.

Stacked on Significant-Gravitas#13687 (base retargets to `dev` once parents merge).

### 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:
- [x] `marketplace/__tests__/experts-section.test.tsx` — 2 passed
(render + hire flow; hired state)
- [x] `marketplace/__tests__/install-on-expert.test.tsx` — 3 passed
(install, duplicate, hidden without experts)
- [x] `team/__tests__/main.test.tsx` — 6 passed (Autopilot first, expert
cards, Chat links, empty state, flag-off 404)
- [x] `copilot/__tests__/expert-threads.test.tsx` — 7 passed (expert_id
on create, grouping, thread header, plain sessions unchanged)
- [x] Regression: copilot sidebar/messages/page suites + marketplace
main suites re-run green
- [ ] Manual E2E on a running stack with seeded roster (before
undrafting)
  - [ ] Screenshots of the three surfaces (before undrafting)

#### 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**)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…t-Gravitas#13699)

### Why / What / How

**Why.** The wallet popover is `28.5rem` wide with an inline top-up
form, an auto-refill config form, collapsible task groups, embedded
videos and progress bars. That was designed for the classic top navbar.
In the new sidebar layout the wallet lives in the sidebar footer as a
small `$1.00` pill, and opening it throws a panel wider than the sidebar
itself over the page. It's far too heavy for what a user wants there:
how many credits do I have, how do I get more.

**What.** A compact popover used *only* when the wallet renders in the
new layout (`compact` prop, set by `SidebarUserActions`). The classic
navbar keeps the existing panel, byte-for-byte.

**How.** `Wallet.tsx` had grown to 360 lines mixing trigger markup,
popover content, credits fetching, confetti and websocket wiring. Rather
than add a second panel to it, the shared parts were pulled out first:

- `helpers.ts` — task-group definitions + `getEarnRows()`
- `useWallet.ts` — credits, balance-flash, completion counts, onboarding
websocket + confetti
- `components/WalletFullPanel.tsx` — the existing classic panel, moved
verbatim
- `components/WalletCompactPanel.tsx` — the new one

`Wallet.tsx` is now ~120 lines and just picks a panel.

The compact panel shows three things: balance, an **Add credits** row,
and a collapsible **Earn credits** list. To keep the list short,
`getEarnRows()` flattens the three task groups — a group with every task
done collapses to one `First Wins · 3 of 3 — Done` row, while a group
still in progress contributes one row per task.

**Add credits** opens `TopUpDialog` instead of embedding `TopUpForm`
inline. That dialog's copy was hardcoded to *"You're out of automation
credits"*, which is wrong when you open it deliberately with a positive
balance, so it gained an `add-credits` variant. Default is unchanged, so
the low-credit banner and daily auto-opener read exactly as before.

Completed rows reuse `SealCheckIcon` (`weight="fill"`, `#00a656`) from
the copilot `TaskProgressBar`'s "All tasks complete" header, so
completion reads the same across the app.

### Changes 🏗️

- Add `WalletCompactPanel` — balance, Add credits row, collapsible Earn
credits list; rendered when `compact` is set (new layout only)
- Add `getEarnRows()` to flatten task groups; completed groups collapse
to a single Done row
- Extract `helpers.ts` (task groups + row flattening) and `useWallet.ts`
(credits, confetti, websocket) out of `Wallet.tsx`; move the classic
panel to `WalletFullPanel.tsx`. `Wallet.tsx` 360 → ~120 lines
- Compact popover opens `side="top" align="start"` at `22rem` with
`rounded-2xlarge`, sized so long task names wrap rather than truncate
- Add a `variant` prop to `TopUpDialog` (`out-of-credits` default |
`add-credits`) so the wallet entry point gets accurate copy
- Reuse `SealCheckIcon` / `CircleIcon` from the copilot task-progress
treatment for row status
- `Task` / `TaskGroup` interfaces moved from `Wallet.tsx` to
`helpers.ts` (only importer, `WalletTaskGroups.tsx`, updated)

No backend changes. No API changes. Reward amounts are unchanged and
still mirror `backend/data/onboarding.py`.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [ ] I have tested my changes according to the test plan:
  - [x] `pnpm format`, `pnpm lint`, `pnpm types` clean
  - [x] `pnpm test:unit` — 379 files pass
- [x] New `WalletCompactPanel.test.tsx`: row flattening (completed group
collapses, in-progress group lists tasks, no task dropped), panel
render, accordion collapse, Add credits callback
- [ ] **New layout** (`AUTOGPT_NEW_LAYOUT` on): open the wallet from the
sidebar footer — compact panel opens up and to the right, fits the
viewport, long task names wrap
- [ ] Click **Add credits** — popover closes, dialog opens titled "Add
automation credits", Stripe checkout still reachable
  - [ ] Toggle **Earn credits** — list collapses and expands
- [ ] With `ENABLE_PLATFORM_PAYMENT` off — Add credits row is hidden,
rest of the panel still renders
- [ ] **Classic navbar** (`AUTOGPT_NEW_LAYOUT` off): wallet is visually
unchanged — inline top-up, auto-refill, task videos, progress bars,
confetti on step completion
- [ ] Hit zero credits — low-credit banner and daily auto-opener still
say "You're out of automation credits"
…ravitas#13330)

### Why / What / How

**Why:** The platform's only hard dependency on the Supabase stack was
auth (GoTrue + Kong + the `auth.users` schema). That dependency forced
every deployment — including local/self-hosted — to run the full
docker-compose Supabase sidecar, which blocks the longer-term goal of
shipping the platform as a single container or packaged app.

**What:** Replaces Supabase Auth with [Better
Auth](https://better-auth.com) (1.6.x) embedded in the Next.js frontend,
with zero session loss for existing users and zero password resets.

**How:**
- **Auth service** lives in the Next.js server at `/api/auth/*`
(cookie-based DB sessions, tables in the `platform` schema of the
existing Postgres, owned by Prisma migrations — one migration
authority).
- **Backend validation**: the JWT plugin signs ES256 tokens published at
`/api/auth/jwks`; `autogpt_libs.auth` now validates asymmetric tokens
against `JWT_JWKS_URL` while keeping the legacy HS256 shared-secret path
active, dispatched by token algorithm. JWT claims are shaped identically
to GoTrue's (`sub`, `email`, `role`, `user_metadata.name`, `aud:
"authenticated"`), so no route/handler changes were needed.
- **No session loss**: three mechanisms layered —
  1. backend dual-validation keeps in-flight legacy JWTs working,
2. a `supabase-bridge` Better Auth plugin silently upgrades a legacy
Supabase cookie (signature-verified, accepted up to
`SUPABASE_BRIDGE_MAX_TOKEN_AGE_DAYS` past expiry since refresh tokens
have no issuer anymore) into a Better Auth session, triggered by
middleware exactly once per browser,
3. `scripts/migrate-supabase-auth.ts` copies
`auth.users`/`auth.identities` into the Better Auth tables preserving
UUIDs (no FK orphans) and bcrypt hashes (Better Auth is configured with
bcrypt hashing, so GoTrue passwords keep verifying).
- **Token plumbing**: browser API calls already go through `/api/proxy`,
so backend JWTs are minted server-side from the cookie session (with a
per-session mint cache) — the browser never holds a JWT.
- **Compose**: `kong`, `auth` (GoTrue), `studio`, `meta` removed; `db`
is now plain `pgvector/pgvector:pg16` with an init shim that creates a
minimal `auth.users` so the historical migration chain applies on fresh
databases.

### Changes 🏗️

- `autogpt_libs.auth`: JWKS (ES256/RS256/EdDSA) validation via
`JWT_JWKS_URL` + legacy `JWT_VERIFY_KEY` HS256 path; config validation
requires at least one
- Prisma: Better Auth tables (`UserAuthIdentity`, `UserAuthSession`,
`UserAuthAccount`, `UserAuthVerification`, `UserAuthJwks`) + migration
that also drops the `auth.users` sync trigger
- Backend: removed `supabase` SDK; feature-flag LD context now reads the
auth user table; e2e test data seeds bcrypt credential accounts
- Frontend: new `src/lib/auth` module (server config w/ admin+jwt+bridge
plugins, actions, hooks, middleware); all ~50 `useSupabase` consumers
migrated to `useAuth` (same surface);
login/signup/OAuth/password-reset/email-change flows rewritten;
`@supabase/*` packages removed
- Infra: compose without the Supabase stack; CI provisions plain
pgvector Postgres; env defaults updated (see below); installer + docs
updated
- kysely pinned to 0.28.17 (upstream `@better-auth/kysely-adapter`
imports a constant kysely 0.29 removed from the root export)

**Configuration changes:**
- Backend: `+JWT_JWKS_URL`, `SUPABASE_URL`/`SUPABASE_SERVICE_ROLE_KEY`
removed, `JWT_VERIFY_KEY` retained for the migration window
- Frontend: `+BETTER_AUTH_SECRET`, `+BETTER_AUTH_URL`, `+DATABASE_URL`,
`+AUTH_DB_SCHEMA`, optional
`AUTH_{GOOGLE,GITHUB,DISCORD}_CLIENT_ID/SECRET`, optional `SMTP_*`,
optional `SUPABASE_JWT_SECRET` (bridge window only);
`NEXT_PUBLIC_SUPABASE_*` removed
- OAuth provider consoles need redirect URIs updated from GoTrue
(`/auth/v1/callback`) to
`https://<frontend>/api/auth/callback/{provider}`

**Cloud rollout order (automated on merge):** the deploy workflow's
`migrate` job runs the schema migrations *including the bulk
GoTrue→Better Auth copy*
(`20260716120000_copy_supabase_users_to_better_auth`), and the backend
deploy dispatches only after it succeeds. The Vercel frontend build runs
in parallel off the same push — if it finishes first, auth 500s briefly
until the migrate job lands (self-heals; no action needed). After the
frontend flip, run the **“AutoGPT Platform - Supabase Auth Sweep”**
workflow (workflow_dispatch, environment picker) once to copy any users
who signed up via GoTrue in between. Keep `SUPABASE_JWT_SECRET` set
(`JWT_VERIFY_KEY` falls back to it) through the bridge window → after
the window, drop the legacy secrets and the GoTrue infra.

### 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:
- [x] Fresh plain-Postgres DB: full Prisma migration chain applies (auth
shim verified)
- [x] Signup via Better Auth → UUID user + session cookie; platform User
created via backend with same id
- [x] `/api/auth/jwks` serves ES256 key; minted JWT carries
`sub`/`email`/`role`/`user_metadata`/`aud=authenticated`
- [x] Python backend accepts the ES256 JWT via JWKS (`POST
/api/auth/user` → 200)
- [x] Legacy GoTrue-style HS256 JWT still accepted by the backend (dual
validation)
- [x] Bridge: expired-but-valid legacy `sb-*` cookie on a protected page
→ Better Auth session minted, sb cookies cleared, redirected to original
destination
- [x] bcrypt sign-in 200, wrong password 401; protected pages redirect
without session, render with one; `/api/proxy` mints backend JWT
(credits endpoint 200)
- [x] Backend: 627 util tests + user-data tests green; autogpt_libs auth
suite green (new JWKS tests incl. rotation/kid-miss)
- [x] Frontend: `pnpm types`/`lint` clean; 3215+ unit tests green incl.
new bridge/middleware/mapping tests

#### 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**)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

### Cloud deploy checklist ☁️

Things that live outside this repo's code and must be handled at rollout
(in order):

1. **Waitlist**: signup gating was a Postgres trigger on `auth.users`;
new signups insert into `platform."user"` where no trigger exists. Port
the waitlist check (trigger on `platform."user"` or a Better Auth
`databaseHooks.user.create.before` hook) **before** deploy, or signups
are open. The frontend's `not_allowed` error handling is preserved and
ready.
2. **Infra repo env/manifests**: set `BETTER_AUTH_SECRET` (the compose
default is dev-only), frontend `DATABASE_URL` +
`AUTH_DB_SCHEMA=platform`, backend `JWT_JWKS_URL`, SMTP credentials
(Postmark SMTP works; without SMTP, password
reset/verification/email-change flows fail loudly by design); keep
`JWT_VERIFY_KEY` + `SUPABASE_JWT_SECRET` set for the bridge window;
remove GoTrue/Kong workloads after the window.
3. **OAuth consoles**: change Google/GitHub/Discord redirect URIs from
GoTrue `/auth/v1/callback` to
`https://<frontend>/api/auth/callback/{provider}`, then smoke-test each
provider in staging — including a sign-in by a *migrated* OAuth user
(identity rows are migrated and account linking is enabled, but no live
provider flow runs in CI).
4. **Data migration**: automated — the bulk copy rides the deploy
workflow's migrate job. Post-flip, run the “AutoGPT Platform - Supabase
Auth Sweep” workflow once for stragglers (idempotent / re-runnable;
`frontend/scripts/migrate-supabase-auth.ts` under the hood).
5. Known accepted trade-offs: bridge accepts signature-valid Supabase
tokens ≤30 days past expiry (`SUPABASE_BRIDGE_MAX_TOKEN_AGE_DAYS`); open
SPA tabs at cutover 401 until one page reload; migrated *temporary* bans
become permanent; GoTrue 2FA enrollments don't migrate (no 2FA UI exists
today).
6. Post-merge hardening: back Better Auth's rate limiter with Redis for
multi-replica (per-instance memory by default); admin role assignment is
now `role='admin'` on `platform."user"`; delete the inert `db/docker/`
tree in a cleanup PR.


### Rollback ⏮️

**Precondition — the rollback window:** keep the GoTrue/Kong workloads
**and** `JWT_VERIFY_KEY` + `SUPABASE_JWT_SECRET` running until the
cutover is confirmed healthy. Dropping the legacy infra/secrets (final
step of the rollout order) is the **point of no return** — don't do it
until you're confident.

1. **Roll the frontend back in Vercel** — Instant Rollback to the
previous deployment of the environment (or revert the merge commit on
the branch). The backend needs **no** revert — dual-validation still
accepts GoTrue HS256 tokens, and the old frontend re-issues them.
(Backend JWKS fetches fail while rolled back, which is what makes
already-bridged ES256 sessions 401 as described below — expected, not a
fault.)
2. **Sessions:** anyone still holding a valid GoTrue cookie continues
uninterrupted; anyone who had already bridged to a Better Auth session
401s once → one reload → re-logs-in via GoTrue (their `auth.users`
credential still exists).
3. **No DB rollback needed** — leave the Better Auth tables in place;
the old system ignores them.

**One-way caveat:** users who **signed up or changed their password on
Better Auth during the cutover window** exist only in
`platform."user"`/`account`, not `auth.users`, so after rollback they
can't log in via GoTrue. Mitigate by picking one: (a) keep the window
short and accept the few re-signups, (b) freeze new signups during the
window, or (c) reverse-sync new `platform."user"` rows into `auth.users`
before rolling back.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **High Risk**
> This is a full authentication and deployment-model change (JWT
verification, session migration, CI database setup) touching
security-critical paths; rollout order and env secrets (`JWT_JWKS_URL`,
bridge window) matter for production access.
> 
> **Overview**
> **Replaces the Supabase auth stack** (Kong, GoTrue, Studio, related
env) with **plain `pgvector` Postgres** and **Better Auth** on the
frontend. Platform `.env.default` is trimmed to database credentials;
local DB data moves to `data/db/data`.
> 
> **`autogpt_libs.auth` is reworked for Better Auth:** `JWT_JWKS_URL` is
**required** at startup (with cleartext remote `http://` blocked unless
`JWKS_ALLOW_INSECURE_TRANSPORT`). Tokens verify via **JWKS (ES256,
etc.)** or legacy **HS256** (`JWT_VERIFY_KEY` / `SUPABASE_JWT_SECRET`)
by algorithm. New **`requires_frontend_service`** guards pre-login
backend calls with JWKS-signed service tokens (separate audience from
user JWTs). Missing `role` claims **fail closed** (403 admin, default
`"user"` elsewhere) instead of `KeyError`/500.
> 
> **CI and workflows:** Docker image caches swap Supabase images for
**`pgvector/pgvector:pg15`**; backend tests use a **pgvector container +
init shim** instead of `supabase start`; pytest drops `SUPABASE_*` env;
new **`autogpt-libs-test`** job. Full-stack E2E starts **`db` only** (no
GoTrue), caches/dumps **platform schema** (Better Auth tables), and
**fails** if `rest_server` is unhealthy after 180s. Adds **manual
“Supabase Auth Sweep”** workflow running `migrate-supabase-auth.ts`
post-cutover.
> 
> Also: **`SECURITY.md`** clarifies JWKS transport warnings are operator
guidance; **`.gitignore`** for `supabase/.temp/`; removal of
**`supabase_integration_credentials_store/types.py`** from the diff
hunk.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ffc0264. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… useChat state (Significant-Gravitas#13766)

### Why / What / How

<!-- Why: Why does this PR exist? What problem does it solve, or what's
broken/missing without it? -->
**Why:** This PR addresses the `TypeError: Cannot read properties of
undefined (reading 'state')` occurring in the `/copilot` section of the
application. The error originated in
`frontend/src/app/(platform)/copilot/useCopilotStream.ts` within the
`resumeStreamFromStart` function. This function was calling
`resumeStream()` from the `@ai-sdk/react`'s `useChat` hook. The
`useChat` hook was sometimes initialized with a fallback `{ id: "new" }`
configuration when the `chatRuntime` object was `null` (e.g., if
`sessionId` was falsy or `getOrCreateCopilotChatRuntime` returned null).
In this uninitialized state, calling `resumeStream()` would cause an
internal crash as it attempted to read `.state` from an `undefined`
object.
<!-- What: What does this PR change? Summarize the changes at a high
level. -->
**What:** To prevent this, a null guard for `chatRuntime` has been added
to `resumeStreamFromStart()`.
<!-- How: How does it work? Describe the approach, key implementation
details, or architecture decisions. -->
**How:** An `if (!chatRuntime) return;` statement was inserted as the
first line within the `resumeStreamFromStart` function, ensuring that
`resumeStream()` is only called when a valid `chatRuntime` object is
present.

### Changes 🏗️

<!-- List the key changes. Keep it higher level than the diff but
specific enough to highlight what's new/modified. -->
- Added a null guard (`if (!chatRuntime) return;`) to the
`resumeStreamFromStart` function in
`frontend/src/app/(platform)/copilot/useCopilotStream.ts` to prevent
calling `resumeStream()` when `chatRuntime` is null.

### Checklist 📋

#### For code changes:
- [x] 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:
  <!-- Put your test plan here: -->
  - [ ] Navigate to the `/copilot` section.
- [ ] Verify that the `TypeError: Cannot read properties of undefined
(reading 'state')` no longer occurs, especially when `chatRuntime` might
be null or uninitialized.
- [ ] Ensure the copilot chat functionality works as expected under
normal conditions.

<details>
  <summary>Example test plan</summary>
  
  - [ ] Create from scratch and execute an agent with at least 3 blocks
- [ ] Import an agent from file upload, and confirm it executes
correctly
  - [ ] Upload agent to marketplace
- [ ] Import an agent from marketplace and confirm it executes correctly
  - [ ] Edit an agent from monitor, and confirm it executes correctly
</details>

#### For configuration changes:

- [ ] `.env.default` is updated or already compatible with my changes
- [ ] `docker-compose.yml` is updated or already compatible with my
changes
- [ ] I have included a list of my configuration changes in the PR
description (under **Changes**)

<details>
  <summary>Examples of configuration changes</summary>

  - Changing ports
  - Adding new services that need to communicate with each other
  - Secrets or environment variable changes
  - New or infrastructure changes such as databases
</details>

Fixes
[BUILDER-7V4](https://significant-gravitas.sentry.io/issues/7432474741/?seerDrawer=true)

Co-authored-by: seer-by-sentry[bot] <157164994+seer-by-sentry[bot]@users.noreply.github.com>
## Summary

Automates CLA tracking with labels and reminders for open PRs.

## What it does

### Labels
- Creates `cla: pending` 🟡 and `cla: signed` 🟢 labels
- Auto-applies based on `license/cla` check status
- Makes it easy to filter PRs by CLA status

### Timeline for unsigned CLAs
| Day | Action |
|-----|--------|
| 0 | PR opened → `cla: pending` label added |
| 7 | Reminder comment with signing link + troubleshooting tips |
| 23 | Warning: "PR will close in 7 days" |
| 30 | Auto-close with friendly message |

### Triggers
- **Real-time:** `check_run` completed (catches CLA status changes)
- **PR events:** opened, synchronize, reopened  
- **Daily sweep:** 9 AM UTC (catches edge cases)
- **Manual:** `workflow_dispatch` for testing

## Sample Comments

<details>
<summary>7-Day Reminder</summary>

> 👋 **Friendly reminder:** This PR is waiting on a signed CLA.
> 
> All contributors need to sign our Contributor License Agreement before
we can merge this PR.
> 
> **➡️ [Sign the CLA
here](https://cla-assistant.io/Significant-Gravitas/AutoGPT?pullRequest=123)**
> 
> <details>
> <summary>Why do we need a CLA?</summary>
> 
> The CLA protects both you and the project by clarifying the terms
under which your contribution is made. It's a one-time process — once
signed, it covers all your future contributions.
> 
> </details>
> 
> <details>
> <summary>Common issues</summary>
> 
> - **Email mismatch:** Make sure your Git commit email matches your
GitHub account email
> - **Merge commits:** If you merged `dev` into your branch, try
rebasing instead: `git rebase origin/dev && git push --force-with-lease`
> - **Multiple authors:** All commit authors need to sign, not just the
PR author
> 
> </details>
> 
> If you have questions, just ask! 🙂

</details>

<details>
<summary>23-Day Close Warning</summary>

> ⚠️ **This PR will be automatically closed in 7 days** if the CLA is
not signed.
> 
> We haven't received a signed CLA from all contributors yet. Please
sign it to keep this PR open:
> 
> **➡️ [Sign the CLA
here](https://cla-assistant.io/Significant-Gravitas/AutoGPT?pullRequest=123)**
> 
> If you're unable to sign or have questions, please let us know — we're
happy to help!

</details>

<details>
<summary>30-Day Auto-Close</summary>

> 👋 Closing this PR due to unsigned CLA after 30 days.
> 
> Thank you for your contribution! If you'd still like to contribute:
> 
> 1. [Sign the
CLA](https://cla-assistant.io/Significant-Gravitas/AutoGPT)
> 2. Re-open this PR or create a new one
> 
> We appreciate your interest in AutoGPT and hope to see you back! 🚀

</details>

## Configuration

Timing is configurable via env vars:
```yaml
env:
  REMINDER_DAYS: 7       # Days before first reminder
  CLOSE_DAYS: 30         # Days before auto-close
```

## Testing

Can be tested with:
```bash
gh workflow run cla-label-sync.yml -f pr_number=11919
```

---------

Co-authored-by: Nicholas Tindle <nicholas.tindle@agpt.co>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ntindle <8845353+ntindle@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eader button (Significant-Gravitas#13767)

### Why / What / How

<!-- Why: Why does this PR exist? What problem does it solve, or what's
broken/missing without it? -->
This PR addresses a `NotFoundError` occurring in the `/tour/chat` page
when browser translation extensions (like Google Translate) are active.
The `TourChatHeader` component's `Button` conditionally renders bare
text (`'Link copied'` or `'Share this demo'`). Browser translation
extensions often wrap these bare text nodes in `<font>` tags, mutating
the DOM. When the `isCopied` state changes, React attempts to reconcile
the DOM by inserting or replacing a text node, but it fails because the
original text node is no longer a direct child of the `Button` (it's now
nested within a `<font>` tag), leading to a `NotFoundError`.

<!-- What: What does this PR change? Summarize the changes at a high
level. -->
The fix involves wrapping the conditionally rendered text inside the
`Button` in `TourChatHeader.tsx` with a `<span>` element.

<!-- How: How does it work? Describe the approach, key implementation
details, or architecture decisions. -->
By wrapping the text in a `<span>`, React has a stable DOM element to
update. When the `isCopied` state toggles, React updates the
`textContent` of the `<span>` element, rather than attempting to replace
a bare text node. This approach is resilient to external DOM mutations
by browser extensions, as the `<span>` provides a consistent structural
anchor for React's reconciliation process.

### Changes 🏗️

<!-- List the key changes. Keep it higher level than the diff but
specific enough to highlight what's new/modified. -->
- Wrapped conditionally rendered text ('Link copied' or 'Share this
demo') within the `Button` component in `TourChatHeader.tsx` with a
`<span>` element.

### 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:
  <!-- Put your test plan here: -->
  - [x] Navigate to the `/tour/chat` page.
- [x] Activate a browser translation extension (e.g., Google Translate)
and translate the page.
  - [x] Click the 'Share this demo' button.
- [x] Verify that the text changes to 'Link copied' without any
`NotFoundError` in the console.
- [x] Click the button again and verify the text reverts to 'Share this
demo' without errors.

<details>
  <summary>Example test plan</summary>
  
  - [ ] Create from scratch and execute an agent with at least 3 blocks
- [ ] Import an agent from file upload, and confirm it executes
correctly
  - [ ] Upload agent to marketplace
- [ ] Import an agent from marketplace and confirm it executes correctly
  - [ ] Edit an agent from monitor, and confirm it executes correctly
</details>

#### For configuration changes:

- [ ] `.env.default` is updated or already compatible with my changes
- [ ] `docker-compose.yml` is updated or already compatible with my
changes
- [ ] I have included a list of my configuration changes in the PR
description (under **Changes**)

<details>
  <summary>Examples of configuration changes</summary>

  - Changing ports
  - Adding new services that need to communicate with each other
  - Secrets or environment variable changes
  - New or infrastructure changes such as databases
</details>

Fixes
[BUILDER-7WC](https://significant-gravitas.sentry.io/issues/7439524689/?seerDrawer=true)

Co-authored-by: seer-by-sentry[bot] <157164994+seer-by-sentry[bot]@users.noreply.github.com>
…ignificant-Gravitas#13627)

### Why 🤔

Model management was scattered across three hand-maintained dicts
(`MODEL_METADATA`, `MODEL_COST`, `TOKEN_COST`), env vars, LD payloads,
and handwritten retirement migrations. Adding one model meant editing
four places; retiring one meant writing SQL. This PR makes one file —
`backend/data/llm_registry/catalog.py` — the single source of truth for
every model fact, and proves the swap changes nothing that wasn't meant
to change.

Consolidates and supersedes the stacked PRs Significant-Gravitas#13621–Significant-Gravitas#13626 (same commits,
one landing) and Significant-Gravitas#13596 (K3 ships here, through the new path).

### Changes 🏗️

**The catalog system** (from the reviewed stack):
- `catalog.py` — 86 models / 8 providers / 17 creators with metadata,
credit pricing, visibility (GA/employees/admins/hidden), enable flags,
and copilot routing cells (ship empty; env stays authoritative until a
cell is claimed)
- Copilot model resolution: LD override → catalog cell (cloud-only) →
env default, with serve-time gating (unknown/disabled slugs refused
loudly) and `ChatMessage.model`/`routingSource` stamping for
product-intelligence
- Retirement CLI (`python -m backend.data.llm_registry.retire`) —
rewrites stored graphs with a revertible record; kills
retirement-by-migration
- One migration: ChatMessage stamp columns + `LlmModelMigration` records
table
- Docs: `docs/platform/contributing/managing-llm-models.md` runbook

**The cutover (this PR removes what it replaces):**
- `MODEL_METADATA`, `MODEL_COST`, `TOKEN_COST` literals (~600 lines)
deleted; same names now derived from the catalog at import, so
blocks/billing/stagehand callers are untouched
- Drift tripwires deleted (nothing left to drift); replaced by a
cutover-parity snapshot test proving billing + metadata identical for
all 85 pre-existing models
- `LlmModel` stays as pure identifiers (block schemas serialize it); an
import-time check keeps enum ⊆ catalog
- The public `GET /api/llm/catalog` endpoint from the original stack is
**dropped entirely** (−793 lines: routes, per-IP rate limiter, XFF trust
logic, cache-path carve-out, settings). It existed to bootstrap
DB-seeded installs — a premise the in-repo catalog removes. A future
model picker gets a small authenticated route instead (documented in the
runbook).

**Kimi K3** — first model added the new way:
- One catalog entry (`moonshotai/kimi-k3`, 1M ctx, tier 3, run 9cr,
450/2250 cr per 1M) + one enum name line
- Split-price Moonshot rate override ($3/$15 per Mtok, verified against
live OpenRouter) for copilot in-turn cost estimates

**Post-review hardening** (three parallel deep-review agents:
correctness / architecture-skeptic / goals-audit):
- Model identity moved into the catalog package
(`llm_registry/llm_models.py`) — identity and facts share one home;
`blocks/llm.py` re-exports, zero call sites changed. `DEFAULT_LLM_MODEL`
now derives from the catalog's `is_recommended`; kill-switched models
drop out of picker metadata while stored graphs keep validating.
- Router fixes: catalog gating is cloud-only (self-hosted operators' LD
slugs serve again), date-suffixed Anthropic catalog slugs match their
OpenRouter spellings (`anthropic/claude-haiku-4-5` ↔
`claude-haiku-4-5-20251001`), Sentry refusals deduped to once per slug
per process.
- Perimeter cut (~250 lines): write-only `route_warnings` Redis sink,
consumer-less registry picker API, shapeless `capabilities`/`metadata`
dict bags.
- Catalog now also carries provider USD list prices
(`provider_*_usd_per_1m`); `moonshot.py`'s split-price overrides read
them (Kimi K3 $3/$15). Retire CLI defaults `--replacement` from
`fallback_model_slug`, making the field's documented behavior true.
- Runbook/AGENTS/ollama docs rewritten for the post-cutover world; the
snapshot's 4 intentional provider display renames (`V0`→`v0 by Vercel`,
`AI/ML`→`AI/ML API`) are disclosed in the parity test itself.

### Proof it's safe 🧪

- **Billing parity**: `pre_catalog_costs_snapshot.json` + test —
catalog-derived costs byte-identical to the deleted dicts for every
model × rate field. Only delta anywhere: two provider *display* names
improved (`V0`→`v0 by Vercel`, `AI/ML`→`AI/ML API`)
- **No stranded graphs**: `stored_graph_models_test.py` — every distinct
`constantInput->>'model'` value from **real prod and dev databases** (92
values, queried 2026-07-20) resolves through the catalog or is owned by
a non-LLM block
- 126 targeted tests green locally (registry, catalog guards, router,
moonshot); full formatter chain incl. pyright clean

### 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:
  - [x] Parity snapshot test (85 models × all cost/metadata fields)
  - [x] Prod/dev stored-graph fixture test (92 values)
  - [x] Registry/router/retirement suites green locally
- [x] Preview deploy: verified live by review-bot QA (hosted + local
variants) — catalog boots fail-hard with 86 models, dropdown intact
incl. K3, copilot serves end-to-end, dropped endpoint 404s, migration
applies cleanly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Hx4NPwAHAMWU78bfoV3C2b

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **High Risk**
> Changes core copilot routing, billing/metadata derivation, and API
boot behavior; misconfigured catalog or routing cells can block startup
or mis-route production turns until fixed.
> 
> **Overview**
> This PR **cuts over** LLM identity and facts from the giant inline
`blocks/llm.py` enum/metadata dicts to **`backend/data/llm_registry/`**
(`catalog.py` + `llm_models.py`), with **`blocks/llm.py` re-exporting**
`LLMModel`, `DEFAULT_LLM_MODEL`, `MODEL_METADATA`, and related symbols
so block code keeps the same import surface. Call sites rename
**`LlmModel` → `LLMModel`**; admin execution analytics now marks the
recommended dropdown option from **`DEFAULT_LLM_MODEL`** instead of a
hardcoded slug.
> 
> **Copilot model resolution** becomes **`resolve_model_route`**:
LaunchDarkly → catalog **copilot** routing cells → env defaults,
returning **`ResolvedModel(model, source)`**. On **managed cloud**
transports, unknown or kill-switched slugs are **refused** (log +
deduped Sentry) and fall through; **self-hosted** and **local**
baselines skip catalog cells/gating. **`load_catalog()`** runs at **REST
API** startup and in the **copilot executor** (fail-hard if broken).
> 
> **Analytics persistence** adds assistant-turn **`model`** and
**`routingSource`** on chat messages (baseline + SDK paths), with
**back-fill** for rows flushed mid-turn before stamping;
**`routing_source`** stays server-only in API payloads. **Moonshot**
split pricing (e.g. **Kimi K3**) is pulled from catalog provider USD
fields instead of a hand-maintained override map.
> 
> Docs/ops updates describe **catalog-only hotfixes to `master`**, env
vs catalog precedence for AutoPilot, and the **managing-llm-models**
runbook in AGENTS.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
c69a74b. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Bentlybro <Github@bentlybro.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…dBlock (Significant-Gravitas#12702)

### Why / What / How

**Why:** VideoDownloadBlock (currently disabled) has three security
vulnerabilities identified in PR Significant-Gravitas#11796 review and tracked as
SECRT-1898: no SSRF protection on URL input, no download size limit in
yt-dlp options, and no playlist restriction. These must be fixed before
the block can ever be re-enabled.

**What:** Adds SSRF validation, download size enforcement, and playlist
restriction to VideoDownloadBlock, plus 10 tests covering all attack
vectors.

**How:** Reuses the existing `validate_url_host()` from
`backend.util.request` (which blocks private IPs, non-http protocols,
cloud metadata endpoints) by calling it in `run()` before yt-dlp touches
the URL. Adds `max_filesize` and `noplaylist` to yt-dlp's `ydl_opts` so
limits are enforced during download rather than after. Promotes
`MAX_FILE_SIZE_BYTES` to a module-level export in `util/file.py` for
reuse.

### Changes 🏗️

- **`backend/blocks/video/download.py`**: Added `validate_url()` method
wrapping `validate_url_host()`, called in `run()` before
`_download_video()`. Added `max_filesize: MAX_FILE_SIZE_BYTES` and
`noplaylist: True` to `ydl_opts`.
- **`backend/util/file.py`**: Promoted `MAX_FILE_SIZE_BYTES` from local
variable inside `store_media_file()` to module-level constant for import
by other modules.
- **`backend/blocks/video/download_test.py`** (new): 10 tests across 4
classes — SSRF vectors (file://, loopback, private ranges, cloud
metadata, IPv6), size limit verification, playlist restriction, and
valid URL passthrough.

Block remains `disabled=True` pending sandbox work.

### 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:
- [x] All 10 new tests pass (`poetry run pytest
backend/blocks/video/download_test.py -xvs`)
- [x] Formatter and linter pass (`poetry run format && poetry run lint`)
  - [ ] CI passes on this PR

#### 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 needed.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches security-sensitive URL handling and download enforcement for
`yt-dlp`; incorrect validation/limits could still allow SSRF/DoS if the
block is re-enabled, though scope is localized to this block and shared
file-size constant.
> 
> **Overview**
> Adds SSRF protection to `VideoDownloadBlock` by validating the input
URL via `validate_url_host()` before invoking `yt-dlp`, and surfaces
validation failures as `BlockExecutionError`.
> 
> Hardens downloads by passing `max_filesize` (using the newly exported
`MAX_FILE_SIZE_BYTES`) and `noplaylist=True` into `yt-dlp` options to
prevent oversized downloads and playlist pulls.
> 
> Introduces `download_test.py` coverage for blocked URL vectors, max
filesize option wiring, playlist restriction, and a success path, and
promotes `MAX_FILE_SIZE_BYTES` in `backend/util/file.py` to a
module-level constant for reuse.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
d958221. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ded (Significant-Gravitas#13751)

> Only for testing purpose

### Why / What / How

**Why:** We're standardising on [Hugeicons
stroke-rounded](https://hugeicons.com/icons/stroke-rounded) as the
platform icon set. Phosphor was the previous standard and was used in
382 frontend files, so the switch has to be done in one pass — a partial
migration would leave two icon systems with visibly different stroke
weights side by side.

**What:** Every Phosphor icon in the frontend is replaced by its
Hugeicons stroke-rounded equivalent, rendered through a new `Icon` atom
that applies a uniform **2px** stroke width. `@phosphor-icons/react` is
removed from `package.json`.

**How:** Phosphor icons are React *components*; Hugeicons ship as icon
*data* (`IconSvgElement` — an array of SVG path tuples) rendered by a
single component. That difference drives the shape of the whole diff:

```tsx
// before
<TrashIcon size={16} weight="bold" className="text-red-500" />

// after
import { Delete02Icon } from "@hugeicons/core-free-icons";
import { Icon } from "@/components/atoms/Icon/Icon";

<Icon icon={Delete02Icon} size={16} className="text-red-500" />
```

The mechanical rewrite was done with a `ts-morph` AST codemod (not regex
— many Phosphor exports are common words like `Check`, `File`, `X`,
`Table`, and a textual replace would have corrupted strings and JSX
text). It rewrote 852 JSX sites and 321 value/type references. The
remaining ~135 sites — places where an icon was stored in a variable or
record and rendered indirectly — were fixed by hand, since they need a
real decision about whether the field should become `IconSvgElement` or
stay a component type.

Stroke width lives in exactly one place (`ICON_STROKE_WIDTH` in the
`Icon` atom) rather than being repeated on ~850 call sites, so changing
it later is a one-line edit.

### Changes 🏗️

- **New `Icon` atom** (`src/components/atoms/Icon/Icon.tsx`) — wraps
`HugeiconsIcon`, defaults `strokeWidth` to `1.75` and `size` to `"1em"`
so icons keep scaling with surrounding text exactly as Phosphor's did.
Also exports `createIconComponent()` for the few records that
legitimately mix Hugeicons with `react-icons` / `@radix-ui/react-icons`
components and therefore need a uniform component type.
- **Icon mapping** — all 291 distinct Phosphor exports mapped to
Hugeicons equivalents, chosen by visual/semantic match and validated
against the package's export list (e.g. `TrashIcon` → `Delete02Icon`,
`CaretDownIcon` → `ArrowDown01Icon`, `CircleNotchIcon` →
`Loading03Icon`, `MagnifyingGlassIcon` → `Search01Icon`).
- **`weight` prop dropped everywhere** (389 usages). Hugeicons free is
stroke-only and stroke width is now owned by the atom. In the one place
where `weight` encoded state rather than style — connected vs
unconnected node handles in the builder — it's replaced by
`fill={isInputConnected ? "currentColor" : "none"}` so the distinction
survives.
- **Icon-carrying types retyped** from component types to
`IconSvgElement` (nav item configs, artifact classification, sitrep
priority config, etc.).
- `getAccountMenuPhosphorIcon` → `getAccountMenuIcon`, dropping its
now-unused `weight` argument.
- **Docs/config updated** to the new convention: `frontend/AGENTS.md`,
`frontend/CONTRIBUTING.md`, `frontend/README.md`,
`.github/copilot-instructions.md`, and the
`components/tokens/icons.stories.tsx` catalog story.
- **Test mocks migrated** — the four suites that stubbed
`@phosphor-icons/react` now stub the `Icon` atom; two of those mocks
were dead (their testids were never asserted) and were removed.
- `@phosphor-icons/react` removed; `@hugeicons/react` +
`@hugeicons/core-free-icons` added.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [ ] I have tested my changes according to the test plan:
  - [x] `pnpm types` — clean, 0 errors
  - [x] `pnpm lint` — clean, 0 errors
  - [x] `pnpm test:unit` — 405 test files / 4237 tests pass, 2 skipped
  - [x] `pnpm build` — production build succeeds
  - [x] `git grep phosphor` returns nothing outside the lockfile
  - [ ] **Visual review needed** (see below)

#### Visual review

This is the part CI can't cover. The mapping is 1:1 in meaning but
Hugeicons glyphs are not pixel-identical to Phosphor's, and Phosphor's
`weight="fill"` / `"duotone"` variants have no Hugeicons equivalent, so
some previously-filled icons are now outlines. Worth a pass over:

- [ ] Marketplace + Library cards and empty states
- [ ] Copilot chat (tool accordions, artifact panel, chat input chips)
- [ ] Builder canvas — node handles specifically, since
connected/unconnected now reads via `fill` rather than icon weight
- [ ] Settings (sidebar, mobile nav, creator dashboard status pills)
- [ ] Navbar account menu and wallet
- [ ] Storybook `Tokens/Icons` catalog for a side-by-side of the whole
set

A few mappings were judgement calls where Hugeicons has no close twin —
`ChalkboardIcon` → `Presentation01Icon`, `CardsThreeIcon` →
`Album01Icon`, `FoldersIcon` → `FolderLibraryIcon`, `WarningOctagonIcon`
→ `Alert02Icon`. Happy to swap any of these if a reviewer prefers a
different glyph.

#### 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**)
…dream memory (Significant-Gravitas#13391)

## Why

The dream sanitize phase let two kinds of pollution land as durable
memory (observed on the dev canary, Significant-Gravitas#13388):
- **Transient intent** — questions captured as facts: "User is asking
how Kubernetes works", "User is interested in knowing which PRs are
open".
- **Generic world-knowledge** — definitional/tutorial content the
assistant explained in passing ("Kubernetes uses pods as the smallest
unit"), not facts *about the user*.

These dilute warm-context retrieval and are exactly what the sanitizer
(phase 3) exists to drop.

## What / How

Two layers, because prompt-only sanitization was already leaking these:

1. **Sanitize prompt** (`prompts.py`): explicit rules to reject
transient-intent and generic-non-user-knowledge writes/proposals — with
a deliberate carve-out keeping durable user **goals** ("wants to build
X", "is migrating to Y").

2. **Deterministic filter** (`orchestrator.py::_clamp_operations` — the
single sync+batch chokepoint before `apply_operations`) for the
mechanical transient-intent cases the prompt leaks. The regex is
**high-precision by design**: it matches knowledge-seeking intent only
(`asking`, `wondering`, `wants to know/understand/find out`, `interested
in knowing/understanding`, `asked how/what/…`). It deliberately does
**not** match:
   - durable goals (`wants to create/build` — never matched)
- skill goals (`wants to learn Spanish`, `interested in learning Rust`)
— `learn` counts as transient only with an interrogative complement
(`wants to learn HOW X works`)
   - bare `interested in <noun>` ("interested in lighthouses")

Generic world-knowledge is intentionally left to the **prompt**, not the
regex: deciding "is the subject the user?" needs LLM judgment and can't
be regex'd without false positives.

## Review

Adversarially reviewed specifically for false positives (the only real
risk — dropping a legitimate memory). The review found one real
should-fix — `learn`/`interested in learning` bundled with the
interrogative verbs was dropping durable skill goals — which is
**fixed** in this PR (the interrogative-complement carve-out) and pinned
by tests, including bare `interested in <noun>` keep-cases so a future
over-broad edit fails loudly.

Known limitation (nice-to-have, low frequency): a standing notification
preference phrased "wants to know when X happens" is dropped by the
regex; separating it from a one-off "wants to know when the deploy is"
isn't reliably regex-able, so it's left to the prompt + human review.
Noted in a code comment.

## Testing

TDD — 46 passing across `orchestrator_test` + `prompts_test`, including
the keep/drop boundary (transient dropped; goals, skill-goals,
bare-interest, user-facts, and generic-knowledge all kept), the real
`_clamp_operations` integration path for both writes and proposals, and
prompt-rule presence. `pyright`/`black`/`isort` clean.

> Authored while the requester is away and can't review closely;
required approval still gates merge. The one judgment call
(high-precision regex + prompt-for-the-rest, rather than an aggressive
filter that risks eating real memories) is called out above for a
reviewer.

Closes Significant-Gravitas#13388

🤖 Generated with [Claude Code](https://claude.com/claude-code)


Fixes SECRT-2478

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Krzysztof Czerwinski <kpczerwinski@gmail.com>
…ptional PR coverage refresh (Significant-Gravitas#13434)

## Summary

Adds a manual `workflow_dispatch` trigger to
**`.github/workflows/platform-backend-ci.yml`** so the full backend
**lint / type-check / test + coverage** suite can be run **on demand**
against any branch, and — when that branch has an open PR — can refresh
that PR's coverage status.

```bash
# Plain on-demand run for a branch:
gh workflow run platform-backend-ci.yml --ref <branch>

# Run + attach the coverage upload to an open PR (refreshes its codecov status):
gh workflow run platform-backend-ci.yml --ref <pr-head-branch> -f pr_number=<PR#>
```

## (a) What it does

- **`workflow_dispatch` trigger** on `platform-backend-ci.yml` — runs
the same `test` job as the automatic triggers (full matrix + the
existing Codecov upload) for the dispatched branch's HEAD commit.
- **Optional `pr_number` input**, wired to the Codecov action's
`override_pr` on the upload step. When provided, the dispatched run's
coverage upload is attached to that PR, so Codecov re-evaluates the PR's
`codecov/project/platform-backend` status against the PR base. This lets
a manual run refresh a coverage check that's stale only because the
branch never triggered backend CI (e.g. a frontend/docs-only branch,
where the path filter skips the automatic backend run and Codecov falls
back to carried-forward coverage).
- **Automatic runs are unaffected:** the existing `push` /
`pull_request` (with their `paths:` filters) and `merge_group` triggers
are unchanged, and `override_pr` resolves to empty for every
non-`workflow_dispatch` event, so normal PR/commit detection is
untouched.

## (b) Activation caveat

GitHub only makes a `workflow_dispatch` workflow manually triggerable
once the trigger exists on the repository's **default branch**
(`master`) — even when dispatching another branch via `--ref`. This PR
targets `dev`, so **the trigger stays inert until it propagates `dev →
master`** through the normal cadence. Documented in `TESTING.md`.

## (c) Validation note

Because it isn't dispatchable until it reaches `master`, the `pr_number`
/ `override_pr` refresh path **could not be live-validated in this PR**.
After this lands on `master`, it should be validated with one real
dispatch against an open PR (confirm the
`codecov/project/platform-backend` check refreshes against the PR base).
Note: `codecov.yml` has `require_ci_to_pass: true`, so Codecov waits for
the dispatched run's CI to finish before updating the status.

## Files changed

- `.github/workflows/platform-backend-ci.yml` — `workflow_dispatch`
trigger + `pr_number` input + `override_pr` on the upload step.
- `autogpt_platform/backend/TESTING.md` — "Running backend CI on demand"
(how to invoke, the PR-refresh path, and the activation/validation
caveats).

Scope is intentionally minimal — no `codecov.yml` changes, nothing else
touched.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_014Sw1B531emT5AyhbWp46Hh

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
0ubbe and others added 23 commits August 7, 2026 20:44
… viewport (Significant-Gravitas#13797)

### Why / What / How

**Why:** On `/copilot` in the new layout, opening the Files drawer and
previewing a file (artifact panel) pushes the page wider than the
viewport — a horizontal page scrollbar appears and the panel's
right-edge header buttons (download / all files / close) end up
off-screen.

**Root cause:** `SidebarInset` is a `flex-1` item in the sidebar layout
row but has no `min-w-0`. Its default `min-width: auto` resolves to its
content's intrinsic min width. The copilot right-side panels are
`shrink-0` with fixed widths (context panel 432px default, artifact
panel 640px default and freely resizable/persisted), and the chat
column's intrinsic min is ~480px, so on typical laptop widths the
inset's min-content exceeds the space next to the 292px sidebar. Instead
of shrinking, the inset grows the page past the viewport.

The width clamp added in Significant-Gravitas#13630 can't catch this: it clamps the artifact
panel to `parent.offsetWidth - PANEL_RESERVED_WIDTH`, but the parent row
has already grown along with the inset, so the measurement reports
enough room and the clamp never engages.

**What/How:** Add `min-w-0` to `SidebarInset`'s base classes so the flex
layout can shrink it to the space the sidebar leaves. With the inset
held to the available width, the existing Significant-Gravitas#13630 clamp measures the real
row width and correctly sizes the artifact panel, keeping its close
button in view. `TourCopilot` already works around the same issue
locally with `overflow-hidden` on its inset; this fixes it at the source
for all `SidebarInset` usages.

### Changes 🏗️

- `components/ui/sidebar.tsx`: add `min-w-0` to `SidebarInset` base
classes (one-line CSS fix + comment).

### 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:
  - [x] `pnpm format`, `pnpm lint`, `pnpm types` pass
- [ ] On `/copilot` (new layout) with a chat that produced a document
artifact: open the file preview, resize the panel wide, verify no
horizontal page scrollbar and the panel close button stays visible
- [ ] Verify chat + Files drawer (context panel) still lay out correctly
at 1280px and 1024px widths

No integration test added: the bug is flex-layout behavior (`min-width:
auto` intrinsic sizing) which jsdom does not implement, so it can only
be verified in a real browser.

#### For configuration changes:

- [ ] `.env.default` is updated or already compatible with my changes
- [ ] `docker-compose.yml` is updated or already compatible with my
changes
- [ ] I have included a list of my configuration changes in the PR
description (under **Changes**)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ignificant-Gravitas#13798)

### Why / What / How

**Why:** Since Significant-Gravitas#13787 / Significant-Gravitas#13772, an agent scheduled from an expert's chat
(e.g. asking Maria to create and schedule an agent) claims success and
shows up in the chat `list_schedules` tool, but never appears on the
Team card, the `/team/[expertId]` page, or the chat schedules drawer —
the card keeps saying "No schedules yet".

**What:** Stamp the chat session's `expert_id` on schedules created
through the copilot `run_agent` tool, so expert-chat-created schedules
are attributed like install-time and REST-created ones.

**How:** All expert schedule surfaces filter on `schedule.expert_id`
(`getExpertSchedules` in `team/helpers.ts`, with a fallback join on
`ExpertWorkflow.schedule_id`). The install-time path
(`experts/scheduling.py`) and the REST endpoint (`POST
/graphs/{id}/schedules`) both pass `expert_id` to
`add_execution_schedule`, but the chat tool's `_schedule_agent` did not
— even though the `ChatSession` already carries the expert it is scoped
to. The chat `list_schedules` tool lists all user schedules unfiltered,
which is why the expert could "see" the schedule while every expert
surface could not. Passing `session.expert_id` follows the same
session-anchored pattern already used for org/team tenancy in the same
call. Attribution also means these schedules now count toward the
expert's weekly credit guardrail and get deleted by
`detach_expert_triggers` on archive — previously they escaped both.
Plain Autopilot sessions have `expert_id = None`, so nothing changes for
them.

### Changes 🏗️

- `copilot/tools/run_agent.py`: `_schedule_agent` passes
`expert_id=session.expert_id` to `add_execution_schedule`.
- `executor/scheduler.py`: `GraphExecutionJobArgs.expert_id` comment now
mentions the chat-created source.
- `copilot/tools/_test_data.py`: `make_session` accepts an optional
`expert_id`.
- `copilot/tools/run_agent_test.py`: regression tests — expert-scoped
session stamps `expert_id` on the scheduler call; plain session leaves
it `None`.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [ ] I have tested my changes according to the test plan:
- [ ] `poetry run pytest backend/copilot/tools/run_agent_test.py -k
expert` (not run locally; please verify via CI)
- [ ] Manual: in an expert chat, ask the expert to schedule an agent →
schedule appears on the Team card, `/team/[expertId]`, and the chat
schedules drawer

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ype (Significant-Gravitas#13796)

## Why

The dev deploy is failing and onboarding is broken on dev.
`20260805120000_onboarding_step_to_string` (from Significant-Gravitas#13119) failed to
apply:

```
ERROR: cannot alter type of a column used by a view or rule
DETAIL: rule _RETURN on view analytics.user_onboarding depends on column "completedSteps"
```

Postgres won't retype a column a view depends on. The analytics views
read `completedSteps`, but they are generated out-of-band by `poetry run
analytics-views` (`backend/scripts/generate_views.py`) from
`analytics/queries/*.sql` — so they exist in deployed databases and
never in one built from migrations alone. Local runs and CI both build
from migrations only, which is why this passed everywhere and failed
only on a real environment.

Two consequences today:

1. The columns are still `OnboardingStep[]` while the deployed backend
expects `String[]`, so onboarding reads fail with `ApiError: Error
converting field "completedSteps" ... found incompatible value of
"BUILDER_RUN_AGENT"`.
2. Prisma recorded the migration as failed, so **no migration can apply
to dev** (`P3009`) until it is resolved.

## What

The migration now **drops** the dependent relations and lets
`analytics-views` recreate them. It does not restore them itself.

## How

An earlier revision captured each view's definition, owner and ACL and
restored them. Review showed that to be both unnecessary and wrong: the
deployed `user_onboarding_funnel` still hardcodes the **pre-rename**
step grid, so restoring it verbatim would resurrect a funnel reporting 0
for `ONBOARDING_COMPLETE` and `LIBRARY_RUN_AGENT`. Re-running the
generator instead rebuilds every view from the repo, which already
carries the new names, and re-grants `analytics_readonly`.

Dropping rather than restoring also removes a whole class of restore
bugs raised in review: lost column-level grants (`pg_attribute.attacl`),
lost reloptions and comments, non-round-tripping pretty-printed
viewdefs, ACLs widened by `ALTER DEFAULT PRIVILEGES`, and swallowed
owner-restore failures leaving views owned by the migration role.

Discovery covers materialized views and uses `CASCADE`, so transitive
dependents are handled; `pg_depend` is constrained to `classid =
'pg_rewrite'` to avoid false positives; and `lock_timeout` makes the
drop fail fast rather than queue behind a long-running BI query.

## ⚠️ Required step (dev only, no DB access needed)

The failed `_prisma_migrations` row must be cleared before `migrate
deploy` will do anything — it returns `P3009` until then. Thanks to the
`resolve_rolled_back` input on `platform-autogpt-deploy-dev.yaml`, this
is a workflow dispatch rather than a manual database connection:

```bash
gh workflow run platform-autogpt-deploy-dev.yaml \
  -f git_ref=dev \
  -f resolve_rolled_back=20260805120000_onboarding_step_to_string
```

That job runs `prisma migrate resolve --rolled-back` and then `prisma
migrate deploy` against the dev database using the CI secrets, then
triggers the deploy.

**Order matters: merge this PR to `dev` first.** The workflow checks out
`git_ref`, so if it runs before the merge it will clear the failed row
and then re-apply the *broken* migration, putting the row straight back.

`--rolled-back` (not `--applied`) is correct: Prisma runs each migration
in a transaction and this one aborted on the first `ALTER`, so nothing
from it persisted.

**Local databases** — anyone who pulled `dev` and migrated since Significant-Gravitas#13119
merged will hit "migration … was modified after it was applied", since
this edits that file. Local databases are disposable: `prisma migrate
reset`.

## Does production need manual steps?

**No.** `master` has never applied this migration (newest there is
`20260802120000`), so there is no failed record to clear, and the views
are rebuilt in-migration.

Two things worth checking before promoting, since neither is visible
from here:

**1. The deploy role must be able to drop the views** (`DROP VIEW`
requires ownership or superuser):

```sql
SELECT current_user, (SELECT usesuper FROM pg_user WHERE usename = current_user) AS is_superuser;
```

**2. Enumerate the dependency closure**, to see whether prod carries
dependents beyond the two tracked views. A materialized view, or any
view not in `analytics/queries/`, is dropped by `CASCADE` and **cannot**
be rebuilt by this migration — it would need recreating manually. The
migration raises a warning naming each, but Prisma does not surface
server notices, so check in advance:

```sql
SELECT n.nspname, c.relname, c.relkind
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('v','m') AND EXISTS (
  SELECT 1 FROM pg_depend d JOIN pg_rewrite r ON r.oid = d.objid
  WHERE d.classid = 'pg_rewrite'::regclass AND r.ev_class = c.oid
    AND d.refobjid = '"UserOnboarding"'::regclass
    AND d.refobjsubid IN (
      SELECT attnum FROM pg_attribute
      WHERE attrelid = '"UserOnboarding"'::regclass
        AND attname IN ('completedSteps','notified','rewardedFor')));
```

If that returns only `analytics.user_onboarding` and
`analytics.user_onboarding_funnel`, promotion is fully hands-off.

Recommended sequence: fix dev, confirm the deploy goes green there, then
promote.

## Test plan

Verified against a database rebuilt to the deployed shape — full
migration chain, `analytics` schema, `analytics_readonly`, views created
exactly as `generate_views.py` creates them (`WITH (security_invoker =
false)`), plus a view layered on `user_onboarding_funnel` and a
materialized view on the base table:

- [x] The original statement reproduces the **exact** production error
- [x] Fixed migration applies; all four dependents (incl. transitive
view and matview) dropped via `CASCADE`
- [x] Columns end as `TEXT[]`; `OnboardingStep` dropped; renames
applied; `BUILDER_RUN_AGENT` and other values preserved
- [x] `analytics-views` then recreates the views with `reloptions =
{security_invoker=false}` and `analytics_readonly` SELECT restored
- [x] Funnel reports the **renamed** steps (`LIBRARY_RUN_AGENT` 11,
`ONBOARDING_COMPLETE` 15) — the regression the restore-from-catalog
approach would have shipped
- [x] Backend onboarding tests pass

## Follow-up

Local and CI databases are built from migrations only, so nothing can
catch a dependency on these hand-applied views. Bringing
`analytics/queries/*.sql` into CI setup (or running `analytics-views` as
part of deploy) would close the gap — this is the second time these
views have caused trouble.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ificant-Gravitas#13792)

### Why / What / How

**Why:** A transcription request succeeding is not the same as the user
saying anything. Silence, background noise, STT hallucinations, or a few
seconds of filler all come back as a "successful" transcript — and the
whole personalized pipeline (greeting, suggested prompts, provider
picks) would confidently personalize from garbage.

**What:** Adds a quality gate between transcription and personalized
generation, a recovery UI when the gate rejects, and an offline eval
harness (STT WER corpus + gate accuracy corpus) to measure both the
transcription and the gate.

**How:** The gate is layered so the common cases stay free:
1. **Deterministic reject** — empty/no-letters text, looping transcripts
(low unique-word ratio, high zlib compression ratio) never reach a
model.
2. **Deterministic pass** — clearly substantial text (≥40 words, or
char-based route for space-less scripts) never pays for a model call.
3. **Ambiguous middle** — one cheap Haiku call judges "is there enough
concrete information to personalize from?". Any judge failure (no
client, timeout, malformed output) resolves to a recoverable reject,
never a silent pass.

Rejected dumps keep their audio + transcript on the row (`errorCode` =
`no_usable_speech` / `insufficient_content`, plain string column — no
migration). The frontend shows an insufficient-content recovery state
offering retry / type instead / skip, and the intro card falls back to
Path B instead of greeting the user with content the gate refused.

### Changes 🏗️

**Backend**
- `quality.py` — new layered quality gate (deterministic reject →
deterministic pass → LLM judge)
- `service.py` — gate wired into both voice (`finalize_voice_dump`) and
typed (`finalize_typed_dump`) paths; intro card treats quality-rejected
dumps as Path B
- `brain_dump_gate_eval.py` + `eval_corpus.py` + `eval_data/` — offline
evals: STT word-error-rate corpus (clean / 15dB / 5dB noise) and gate
accuracy corpus (noise/silence fixtures + text cases)
- `EVAL.md` — how to run the evals and current baselines
- Tests: `quality_test.py`, `brain_dump_gate_eval_test.py`, plus
service/route coverage for the reject paths

**Frontend**
- `InsufficientState.tsx` — recovery screen for gate rejections (retry /
type / skip)
- `useBrainDumpStep.ts` / `helpers.ts` — handle `no_usable_speech` /
`insufficient_content` error codes as a recoverable state instead of a
generic failure
- Onboarding funnel analytics: track quality rejections and
typed-vs-suggested prompt usage (`brain-dump-analytics.ts`)
- Intro card / welcome dialog fall back cleanly when the dump was
rejected

### 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:
- [x] Backend unit tests for the gate: deterministic reject (empty,
symbols-only, looping transcript), deterministic pass (long dump,
space-less script), ambiguous → LLM judge, judge failure → recoverable
reject
- [x] Service tests: voice + typed finalize reject paths mark the row
failed and return the error code; transcript/audio retained
  - [x] Route tests: finalize response surfaces `error_code`
- [x] Frontend integration tests: insufficient state renders with
retry/type/skip, error-code mapping, analytics events
- [ ] Gate eval run against `eval_data` corpus (requires LLM credentials
— see EVAL.md)

#### For configuration changes:

- [x] `.env.default` is updated or already compatible with my changes
(optional `BRAIN_DUMP_QUALITY_MODEL` env override, defaults to
`anthropic/claude-haiku-4-5`)
- [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**)
…tas#13771)

### Why / What / How

Hired experts need one editable Soul document that consistently controls
their identity and personality across conversations. This adds persisted
Soul fields, a protected owner-only update API, and a Team-page drawer
for editing an expert's name, identity, voice, and boundaries. Every
message rebuilds the existing expert identity suffix from the latest
owned Expert while preserving the cacheable base prompt, escaping user
XML, and enforcing server-defined protected rules.

### Changes 🏗️

- Prisma migration and Expert API models for voice preferences,
boundaries, and protected rules
- Atomic owner-only Soul updates with templates and archived experts
remaining read-only
- Hiring copies all relevant Soul fields from the selected template
- Dynamic prompt suffix rendering for existing sessions with XML
escaping and permanent protected rules
- Right-side Team Soul editor using generated API hooks, query
invalidation, and success/error feedback
- Static empty "What I've learned" placeholder; memory-note persistence
remains deferred to OPEN-3232
- Team Soul UI remains under the existing `hire-experts` flag; backend
routes follow the existing ungated Expert API convention
- OpenAPI regeneration plus backend and frontend coverage for the new
behavior
- No environment-variable or Docker Compose changes

### 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:
  - [x] Relevant backend suite: 151 passed
  - [x] Backend formatting and linting
- [x] Frontend `pnpm format`, `pnpm lint`, `pnpm types`, and `pnpm
test:unit` in order: 405 files passed, 4,241 tests passed, 2 skipped
  - [x] React Doctor completed; no new Soul drawer finding
- [ ] Manual hire, edit, existing-chat response, and reload verification
with a matching local stack

<details>
  <summary>Example test plan</summary>
  
  - [ ] Create from scratch and execute an agent with at least 3 blocks
- [ ] Import an agent from file upload, and confirm it executes
correctly
  - [ ] Upload agent to marketplace
- [ ] Import an agent from marketplace and confirm it executes correctly
  - [ ] Edit an agent from monitor, and confirm it executes correctly
</details>

#### 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**)

<details>
  <summary>Examples of configuration changes</summary>

  - Changing ports
  - Adding new services that need to communicate with each other
  - Secrets or environment variable changes
  - New or infrastructure changes such as databases
</details>
…ng-first home (Significant-Gravitas#13800)

### Why / What / How

**Why:** Closes Significant-Gravitas#13726, Significant-Gravitas#13728, Significant-Gravitas#13727. The morning test: within 5
seconds of opening the copilot home, a beta user should see what
happened overnight, what was found, and what needs a decision. Today the
home is a greeting plus an activity strip, overnight expert runs are
invisible unless you open each thread, and pending human-in-the-loop
approvals are scattered across thread, builder, and run views.

**What:** Three stacked features on one branch:
1. **Morning briefing job** (Significant-Gravitas#13726, backend) — a daily per-user job
that compiles overnight expert runs, findings (per-run activity
summaries), and pending approvals into one briefing, persists it, and
posts it into the user's primary (non-expert) copilot thread by 9am
local time.
2. **Needs-attention unification** (Significant-Gravitas#13728) — `GET /api/review/pending`
now returns expert attribution (name/avatar), agent name, and deep-link
ids for every pending review, batched (kills an N+1); a reusable
`NeedsAttentionList` organism gives one-tap Approve/Skip with shared
React Query invalidation so home and thread views reconcile.
3. **Home surface v1** (Significant-Gravitas#13727, frontend) — behind the existing
`hire-experts` flag, the copilot home becomes briefing-first: greeting →
briefing card (three sections with deep links) → needs-attention list →
team strip with derived member status → composer. Flag off: behavior is
unchanged (covered by a dedicated flag-off test).

**How:**
- New `UserBriefing` table (unique per user + local date; nullable
`deliveredAt` is stamped only after the session post succeeds, so a
failed post is retried next run instead of skipped) with a typed `GET
/api/briefings/latest` endpoint for the home card; the thread post is
rendered markdown via a new `append_plain_session_message` helper that
mirrors `append_expert_run_message` for the latest non-expert session.
- Scheduling mirrors the dream-system pattern: per-user APScheduler cron
(`0 9 * * *` in the user's timezone), Redis-deduped registration with
timezone-drift detection, registered fire-and-forget from
`stream_chat_post` and re-registered on timezone change. The
`hire-experts` LaunchDarkly flag (same key the frontend evaluates — no
separate briefing flag) gates generation; briefing is skipped when there
is nothing to say (no runs, no pending decisions), with a
plain-agent-activity fallback for users with zero experts.
- Review enrichment resolves attribution in four batched, userId-scoped
queries (executions→expert, graph→library agent, copilot session→expert,
node execs for node ids), three of which run concurrently; fields are
additive/optional so all existing consumers are untouched.
- The home mounts *inside* `EmptySession` behind `Flag.HIRE_EXPERTS` —
it takes the pulse chips' slot and falls back to them. `EmptySession` is
modified for that one conditional mount (it owns onboarding and the
composer recipient picker, so it can't be swapped out wholesale);
everything it rendered before is untouched with the flag off.

### Changes 🏗️

**Backend**
- Prisma migrations: new `UserBriefing` model (`briefingDate @db.Date`,
`content Json`, unique `[userId, briefingDate]` — the only index; every
read is by user + date) + nullable `deliveredAt` delivery marker
- `backend/data/briefing.py` — briefing records data layer (idempotent
create, latest-by-date; `get_latest_briefings` returns the newest few so
the route can fall back past an unreadable row)
- `backend/copilot/briefing/` — content models, pure `compose_briefing`
+ `render_briefing_markdown` (markdown metacharacters in
agent/expert-supplied text are escaped before interpolation),
`generate_and_deliver_briefing` orchestration (its four reads run under
one `asyncio.gather`), per-user cron registration helper
- `backend/copilot/db.py` — `append_plain_session_message` (dedup by
message id, session lock, creates a plain session if none)
- `backend/executor/scheduler.py` — `add_morning_briefing_schedule`
(@expose + client wiring) and job body
- `backend/api/features/briefings/` — `GET /api/briefings/latest`
(router-level auth gate, typed `BriefingResponse`, skips past invalid
stored content to the newest readable briefing)
- `backend/data/human_review.py` + review model — six optional
attribution fields, batched enrichment, N+1 fix
- `backend/data/db_manager.py` — RPC exposure for the new functions
(incl. previously missing `get_pending_reviews_for_user`)
- `util/feature_flag.py` — backend `HIRE_EXPERTS` flag (mirror of the
frontend key); chat routes + `update_user_timezone` registration call
sites

**Frontend**
- No new frontend flag — the home rides the existing `HIRE_EXPERTS` flag
- `src/hooks/useNeedsAttention.ts` + `useProcessReviews.ts` — shared
query + mutation wrapper with dual-key invalidation
- `src/components/organisms/NeedsAttentionList/` — one-tap
approve/decline rows with expert avatars, deep links (built by the
shared `src/lib/review-links.ts`), per-row pending state, and a toast on
both success and failure
- `PendingReviewsList` moved onto `useProcessReviews` so
thread/builder/library approvals update the home count immediately
- `copilot/components/CopilotHome/` — `BriefingCard` (runs + findings;
decisions are left to the needs-attention list below it, and stay in the
thread markdown), `TeamStrip`, shared `ExpertAvatar` molecule, assembled
home behind the flag, mounted from `EmptySession` in the pulse-chips
slot
- `openapi.json` regenerated (briefings endpoint + review model fields)

No `.env.default` or `docker-compose.yml` changes.

### 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:
- [x] Backend: new pytest suites for briefing data layer, plain-session
posts, compose/render (12 cases), scheduling registration (14 cases),
briefings routes (5 cases incl. auth + corrupt content), review
enrichment (real-DB, incl. copilot-session flavor) — all green alongside
pre-existing human-review/user/chat suites (174+ tests in touched areas)
- [x] Frontend: Vitest+RTL+MSW component tests for NeedsAttentionList
(4), CopilotHome assembly (7: greeting, strip fallback, briefing card
sections/links, team strip statuses incl. paused + needs-setup grammar,
needs-attention mount/absence); full copilot suite green (119 files /
1503 tests); `pnpm types` clean
- [x] Flag-off regression: existing
PulseChips/CopilotPage/expert-threads tests pass unchanged, plus
`CopilotHome/__tests__/flag-off.test.tsx` asserting the briefing home
neither renders nor fetches when `hire-experts` is off
- [ ] E2E manual pass with the full stack (briefing cron firing at 9am
local against a running backend) — not run in this worktree

#### 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**)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: abhi1992002 <abhimanyu1992002@gmail.com>
Co-authored-by: Abhimanyu Yadav <122007096+Abhi1992002@users.noreply.github.com>
…ignificant-Gravitas#13804)

### Why / What / How

The brain-dump greeting could greet a user who was already
mid-conversation (`greetingSeen` only covers first sends that went
through the copilot home while the flag was on), the LLM promised
integrations we cannot actually run, and the "still generating" state
was faked by a second hero component that had to render the heading row
identically to the intro card. This PR retires the greeting for anyone
with an existing chat session, feeds the live provider registry into
both LLM jobs, and replaces the hero stand-in with a centered orb that
flies into the intro card heading under a shared framer `layoutId`.

### Changes 🏗️

- **Greeting eligibility**: `get_intro_card` now checks
`get_user_session_count` — one session retires the greeting (and records
`greetingSeen` so the count is paid once). A failure there answers "no"
so a genuinely new user keeps their greeting.
- **`greeting_pending` flag**: the pending state is stated outright
instead of inferred from an empty Path A greeting, so the client can
tell "still coming" from "there isn't one"; polling and the loader gate
read it directly.
- **Shared provider registry** (`onboarding_dump/providers.py`):
extracted from `recommend.py` and now also prepended to the greeting
prompt, so the automations it proposes are ones we can run. New
`intro_test.py` covers prompt assembly, including an empty registry.
- **Latency**: the greeting now runs concurrently with understanding
extraction (`asyncio.gather`), recommendations moved to
`claude-haiku-4-5`, and the onboarding loading screen's give-up ceiling
dropped 60s → 15s.
- **Greeting loader**: new `GreetingLoader` renders a pulsing orb alone,
centered; `EmptyHero` is back to being just the regular hero.
`OnboardingIntroCard` reveals its heading and shares
`GREETING_ORB_LAYOUT_ID` with the loader so the orb travels rather than
being swapped.
- **Connect-tools fallback**: when the recommender answers with nothing,
the panel shows six popular providers under "Popular places to start"
instead of an empty section — only once the job has actually resolved,
so real picks are never swapped out from under the user.
- Regenerated `openapi.json` for `greeting_pending`.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [ ] I have tested my changes according to the test plan:
- [ ] Fresh account through onboarding brain dump → loading screen
advances, copilot shows the centered pulsing orb, then the orb flies
into the greeting heading when it lands
- [ ] Greeting names only real integrations (cross-check against
`/providers`)
  - [ ] Reload copilot after sending one message → no greeting reappears
  - [ ] Existing account with prior sessions → no greeting, regular hero
- [ ] Connect-tools panel with a thin/skipped transcript → "Popular
places to start" with six providers
- [ ] `poetry run pytest backend/api/features/onboarding_dump` and `pnpm
test:unit` for the touched suites
…ce, Build (Significant-Gravitas#13805)

### Why / What / How

**Why:** The Agents, Marketplace and Build tabs give a new user no
orientation — you land on a fleet view, a long storefront, or an empty
canvas with no idea what the tab is for or what to do first. **What:**
Each of those three tabs now shows a single first-visit intro card: an
icon, a short title, one sentence, one way forward and one way out.
**How:** A shared `TabIntroCard` shell plus a thin per-tab wrapper,
gated behind the existing `onboarding-brain-dump` LaunchDarkly flag (off
by default). Dismissal is recorded as a per-tab onboarding step so the
backend record is the source of truth across devices, with a user-scoped
`localStorage` cache covering the window before that write lands.

### Changes 🏗️

- **New shared component** `(platform)/components/TabIntroCard` — card
shell, the `useTabIntroCard` first-visit gate, and `helpers.ts` holding
the step map, the CTA union and the `localStorage` cache. The card
dismisses on "Got it", Escape, and a backdrop click. It moves focus into
itself on open, contains `Tab`/`Shift+Tab` while open, and restores
focus to the opener on close.
- **Three per-tab wrappers**: `AgentsTabIntro` (Library),
`MarketplaceTabIntro` (CTA scrolls the featured carousel into view,
falling back to the full listing when nothing is featured),
`BuildTabIntro` (AutoPilot as the primary route, the existing builder
tutorial as a quiet alternative).
- **Build vetoes the card** when deep-linked into a saved graph
(`?flowID=`) — the intro is about starting something and its tutorial
clears the canvas. Vetoing leaves the step unrecorded, so a later blank
`/build` still gets the introduction.
- **The gate is derived, not latched.** `isOpen` waits on the flag, the
resolved user id, the onboarding record and the local cache, all
evaluated during render. Nothing is copied into state, so the card
cannot flash open for a returning user in the frame before their id
resolves. Dismissal is tracked per user id, so a second account signing
in behind the same mounted hook still gets its own intro. The cache read
sits last in the expression, so a user with the flag off never touches
`localStorage`.
- **Backend**: three new `OnboardingStep` values (`AGENTS_TAB_INTRO`,
`MARKETPLACE_TAB_INTRO`, `BUILD_TAB_INTRO`) added to the enum and to
`FrontendOnboardingStep` so `POST /onboarding/step` accepts them instead
of 422-ing. They are unrewarded, and there is a test asserting the
credit model is never reached for them.
- **Flag**: the tab intros ride the existing
`Flag.ONBOARDING_BRAIN_DUMP` rather than one of their own, so they ship
and roll back with the rest of the new onboarding. No new flag key.
- **Analytics**: `tab_intro_shown` / `tab_intro_cta_clicked` /
`tab_intro_dismissed`, each carrying its tab, so one funnel shows both
which tabs get discovered and whether the CTA earns its place. The tab
and CTA properties are typed unions, not strings.
- Scroll targets for the Marketplace CTA (`FEATURED_SECTION_ID`,
`AGENTS_SECTION_ID`) live in `MarketplaceTabIntro/helpers.ts` so
`FeaturedSection` and `MainMarketplacePage` can carry the ids without
the intro importing its own parent page.

### 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:
- [x] `TabIntroCard` renders its copy, CTA and "Got it"; renders nothing
while closed
- [x] Dismisses on "Got it", on Escape, and on a backdrop click — but
*not* on a click inside the card that drifts out (text selection)
- [x] The CTA fires its own action without also reporting a plain
dismissal
- [x] Focus lands inside the card on open, `Tab`/`Shift+Tab` wrap inside
it rather than reaching the page behind, and focus returns to the opener
on close
- [x] Gate stays closed while the flag is off, before LaunchDarkly
answers, before auth resolves the user, and before the onboarding record
loads
- [x] Gate stays closed when the step was already recorded on another
device, and only consults its own tab's step
- [x] A returning user whose id arrives after mount never opens the card
(and never reports it shown); a first-time user does, exactly once
  - [x] A second account signing in without a remount gets its own intro
- [x] Dismissal records the step, caches locally, never reopens — and
still closes when `localStorage` throws
- [x] Agents CTA steps aside; Marketplace CTA scrolls the carousel,
falls back to the listing when nothing is featured, and still closes
when neither exists; Build CTA routes to `/copilot` and the alternative
launches the builder tutorial
- [x] Build stays out of the way of `?flowID=` and leaves the step
unrecorded for later
  - [x] A blocked analytics host does not break a first visit
- [x] Backend: `POST /onboarding/step` accepts all three new steps,
still 422s on rewarded backend-only steps, and grants no credits for the
tab intros (parametrised)
- [x] No regressions in the touched areas — full library, marketplace,
build, shared-component and services suites green (832 tests)
- [ ] Manual pass in the browser with the flag forced on, across all
three tabs

#### For configuration changes:

- [ ] `.env.default` is updated or already compatible with my changes
- [ ] `docker-compose.yml` is updated or already compatible with my
changes
- [ ] I have included a list of my configuration changes in the PR
description (under **Changes**)

No configuration changes. The feature is read through the existing
`onboarding-brain-dump` LaunchDarkly wiring and defaults to `false`, so
an environment with no flag key behaves exactly as before.
…mmary (Significant-Gravitas#13869)

Part of [SECRT-2523](https://linear.app/autogpt/issue/SECRT-2523) — Home
surface v1 (Home & Briefing).

### Why / What / How

**Why:** `get_user_cost_summary()` is the shared aggregation behind the
usage/cost views, but it only returns money — total cents, billable
runs, failed spend. Any consumer that wants an *activity* chart (runs
over time, split by outcome) or an "avg run duration" stat has to issue
a second query over the same `AgentGraphExecution` rows. The data is
already in the scanned rows; we just weren't projecting it.

**What:** Adds per-status run counts and duration aggregates to the
summary — both at the window level (`UserExecutionCostSummary`) and per
day (`UserDailyCost`) — so a stacked activity chart can be built from
the existing single call.

**How:** Extends the two existing aggregate queries (totals + daily)
with `COUNT(*) FILTER (...)` clauses and a
`SUM((stats->>'walltime')::numeric)`. No new query, no new round trip,
same `WHERE` clause, so the `@@index([userId, isDeleted, createdAt])`
hit is unchanged. Rows map into the models with the same `int()` /
`float()` + `or 0` guards used for the existing columns.

Purely additive: every new field has a default, so existing consumers
and the existing fields' behavior are untouched.

Two things worth calling out, both from review:

- **Duration reads `walltime`, not `duration`.**
`update_graph_execution_stats()` persists
`GraphExecutionStats.model_dump()`, whose field is `walltime` — nothing
writes a `duration` key. The first cut of this PR aggregated
`stats->>'duration'`, which would have been `0` for every real run, and
the pre-existing `top_runs.duration_seconds` projection had the same
bug. Both now read `walltime`, and the test fixture writes the real key
so the assertions actually bite.
- **`total_duration_seconds` ships with its own denominator.**
`run_count` includes QUEUED/RUNNING rows and rows with no stats, which
`SUM` skips — dividing by it biases "avg run duration" downward.
`duration_run_count` counts exactly the rows the sum draws from (same
idea as `walltime_count` in `backend/data/generate_data.py`).

### Changes 🏗️

- `UserDailyCost`: `success_count`, `failed_count`, `review_count`
(default `0`).
- `UserExecutionCostSummary`: `success_run_count`, `failed_run_count`,
`review_run_count`, `total_duration_seconds`, `duration_run_count`
(default `0`).
- Totals SQL: three `COUNT(*) FILTER` clauses (`COMPLETED`;
`FAILED`/`TERMINATED`; `REVIEW`),
`COALESCE(SUM((stats->>'walltime')::numeric), 0)::float`, and `COUNT(*)
FILTER (WHERE (stats->>'walltime') IS NOT NULL)`.
- Daily SQL: the same three per-status `FILTER` counts, per bucket.
- Top-runs SQL: `duration_seconds` now projects `stats->>'walltime'`
(pre-existing bug fix).
- The four aggregate queries moved into `_fetch_totals` /
`_fetch_by_agent` / `_fetch_top_runs` / `_fetch_daily`, each owning its
SQL and row→model mapping, with the shared predicate hoisted to
`_BASE_WHERE`. `get_user_cost_summary` is window validation +
`asyncio.gather` + assembly.
- `openapi.json` re-exported — the new response fields are optional with
defaults, so no frontend change is required.
- Tests extended in `execution_cost_summary_test.py`: the fixture writes
`walltime` (and can omit it entirely), a `REVIEW` run and a stats-less
`RUNNING` run added to the mixed-status fixture (counts, duration sum,
and duration denominator asserted), `FAILED` + `REVIEW` runs added to
the daily-bucket fixture (per-day split asserted), and the empty-window
test asserts the new fields are zeroed.

### 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:
- [x] `poetry run pytest backend/data/execution_cost_summary_test.py` —
5 passed against real Postgres (mixed statuses produce the right
per-status counts, duration sum, and duration denominator; the RUNNING
run stays out of both; empty window returns zeros; existing
cost/daily/dry-run assertions still hold)
- [x] `poetry run pytest backend/api/features/v1_test.py -k cost` — 5
passed, the `/api/v1` cost-summary route is unaffected
- [x] `poetry run format && poetry run lint` clean (ruff, isort, black,
pyright)
- [x] `poetry run export-api-schema` + prettier produces a 5-line diff
(the new field only)
…ignificant-Gravitas#12495)

Requested by @ab20145

Radix UI's `<Select.Item>` throws `Error: A <Select.Item /> must have a
value prop that is not an empty string` when any enum option has
`value=""`. This crashes the `/build` page when a block's schema
includes an empty-string enum option.

## Problem

Sentry issue: **BUILDER-5CS** (3 occurrences since Dec 2025)

`SelectWidget.tsx` (the JSON Schema form widget for enum/select fields)
had two paths producing empty strings:

1. `value={value ?? ""}` — passes `""` as the Select value when the form
value is null/undefined
2. `option.value` mapping — passes backend enum options through without
filtering empty strings

## Fix

- Use `value || undefined` instead of `value ?? ""` — shows placeholder
for unset values instead of crashing
- Filter out empty-string enum options before rendering `SelectItem`s

Single file, minimal change. No behavior change for valid (non-empty)
options.

Resolves SECRT-2157

---
Co-authored-by: Abhimanyu Yadav (@Abhi1992002)
<122007096+Abhi1992002@users.noreply.github.com>

---------

Co-authored-by: Nicholas Tindle <nicholas.tindle@agpt.co>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: majdyz <zamil.majdy@agpt.co>
…as#13758)

### Why / What / How

**Why**

AutoGPT Platform should be runnable as a complete local appliance with
one `docker run`, including FalkorDB-backed memory, without changing the
images or deployment behavior used by hosted infrastructure.

**What**

Adds a dedicated single-container appliance containing the frontend,
backend services, PostgreSQL, a three-node Valkey cluster, RabbitMQ,
FalkorDB, nginx, Supervisor, and Tini.

The appliance persists application and service state under `/data`,
generates installation secrets on first boot, and exposes only nginx on
container port `3000`.

**How**

Buildx Bake builds the existing backend `server` target, then uses it as
the base for `autogpt_platform/single-container/Dockerfile`. Internal
services bind to loopback and Supervisor manages their lifecycle. Health
checks and a fail-stop watchdog allow Docker restart policies to recover
the appliance when a required service cannot recover internally.

The frontend server runs as its own Unix user with a clean environment
and a passwordless local PostgreSQL role limited to the Better Auth
tables and required `User` columns. It does not receive PostgreSQL
superuser credentials. Internal AppService traffic stays loopback-only
inside the appliance trust boundary.

The effective PR diff adds only `autogpt_platform/single-container/`
plus its CI workflow and smoke script. Existing backend source, frontend
source, Dockerfiles, the root `.dockerignore`, and Compose files do not
differ from `dev`.

### Changes 🏗️

- Adds the dedicated appliance Dockerfile, runtime scripts, nginx
configuration, Supervisor configuration, and Bake targets.
- Enables FalkorDB-backed Graphiti memory by default.
- Persists generated application secrets, databases, queues, caches, and
memory data under `/data`.
- Runs startup migrations without downloading Prisma tooling at runtime.
- Adds health checks, required-service recovery, clean shutdown, and
whole-container fail-stop handling.
- Defaults new-account signup to disabled until explicitly enabled by
the operator.
- Isolates the frontend and nginx with dedicated operating-system users
and a least-privilege frontend database role.
- Pins internal topology to loopback so persisted backend configuration
cannot expose internal listeners.
- Adds native `linux/amd64` and `linux/arm64` build, runtime-smoke,
vulnerability-scan, and secret-scan CI.
- Keeps the validation workflow non-publishing.

### Test plan

- [x] Run all 59 appliance Python tests.
- [x] Run ShellCheck for the appliance and smoke scripts.
- [x] Run `bash -n` and `git diff --check`.
- [x] Run the repository pre-commit suite under Node 24, including
backend/frontend type checks and API generation checks.
- [x] Build and smoke-test the exact current-head image on `linux/amd64`
and `linux/arm64`.
- [x] Verify literal `docker run`, `/data` reuse, generated-secret
persistence, FalkorDB graph persistence, frontend database isolation,
and service recovery on the exact current head.
- [x] Pass current-head Trivy vulnerability and embedded-secret scans.

### 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 complete test plan
above.

#### For configuration changes:

- [x] `.env.default` is already compatible with these changes.
- [x] `docker-compose.yml` is already compatible with these changes.
- [x] I have included a list of configuration changes under **Changes**.

### Stack navigation

This appliance is based directly on `dev`. Publication and end-user
documentation are handled separately.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Large new packaging surface with auth, secrets, and DB privilege
boundaries; mitigated by extensive smoke tests and isolation checks, but
it is still a new distribution path operators may run in production.
> 
> **Overview**
> Introduces an **experimental single-container AutoGPT Platform** you
can run with one `docker run`, bundling the frontend, backend services,
PostgreSQL, Valkey, RabbitMQ, FalkorDB (Graphiti memory on by default),
nginx, and Supervisor. State and generated secrets live on a **`/data`
volume**; only **port 3000** is exposed.
> 
> The image is built via **Docker Bake** from the existing backend
`server` target plus a new `autogpt_platform/single-container/` stack
(entrypoint/bootstrap, runtime config, least-privilege **frontend DB
role**, loopback-pinned internal ports, health checks, watchdog).
Operators get **`.env.example`** for public URL, signup policy, and LLM
keys.
> 
> **CI** adds `platform-single-container-docker.yml`: unit tests for
appliance helpers, per-arch image build, a long **end-to-end smoke
script** (health, auth, hostile `backend.json`, isolation,
FalkorDB/nginx recovery), and **Trivy** critical vuln + secret scans.
Hosted multi-container deployments are unchanged.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
2d36895. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude <noreply@anthropic.com>
…ignificant-Gravitas#13742)

### Why / What / How

The new sidebar layout (`isNewLayoutActive`) had already landed for
copilot, library and settings, but several pages still assumed the old
top-Navbar shell: `/admin` rendered the legacy sidebar, `/build` lost
half its canvas to the app sidebar, copilot stacked floating mobile
buttons on top of the inset header, and the marketplace/library pages
forced viewport-width or fixed-width containers that overflowed next to
the sidebar. This PR cleans up those remaining pages, plus two
sidebar/wallet polish items that were queued behind the same layout
work.

The approach keeps the classic layout untouched everywhere: `/admin`
switches between `AdminClassicShell` (legacy sidebar, verbatim move) and
a new `AdminNewShell` that mirrors the `/settings` shell, and every
other change is either gated on `isNewLayoutActive`/`showNewLayout` or a
width fix that is correct in both shells. The builder's collapsed
sidebar is seeded via `SidebarProvider defaultOpen` for hard loads and
restored on exit by a small `BuilderSidebarAutoClose` effect for
client-side navigation.

### Changes 🏗️

- **Admin**: new-layout shell with its own `AdminSidebar` (Back link,
grouped nav) and `AdminMobileNav`; legacy sidebar extracted to
`AdminClassicShell` so it can be deleted when the flag ships.
`PlatformChrome` now renders the bare shell for `/admin` like it already
does for `/settings`.
- **Builder**: `/build` opens with the app sidebar collapsed and
restores the previous open state when you navigate away.
- **Copilot**: floating `MobileHeader` is dropped under the new layout;
the workspace-files toggle moves into the inset header as
`WorkspaceFilesTrigger`, and banners get top padding so they clear the
floating controls.
- **Library**: agent detail becomes a two-column, self-scrolling `lg`
grid (runs list on the right) instead of assuming a top-navbar page.
- **Marketplace**: search/loading/error containers use `w-full max-w-*`
+ responsive padding instead of `w-screen` / `lg:min-w-[1440px]`.
- **Profile**: legacy `/profile/*` pages redirect to their `/settings/*`
equivalents under the new layout.
- **Recent chats**: pinned section + pin/unpin menu action behind the
existing `chat-pinning` flag, and a per-link `useLinkStatus` spinner.
- **Wallet**: "Earn credits" becomes a per-group accordion
(`getEarnRows` → `getEarnGroups`) — completed groups collapse,
in-progress groups stay open and show their remaining reward total.
- `SidebarTrigger` now forwards `className`; `SidebarInset` gets
`min-w-0` so wide content can't push the inset past the viewport.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [ ] I have tested my changes according to the test plan:
- [ ] With the new layout enabled: `/admin/*` shows the new sidebar
shell (desktop) and mobile nav (narrow), Back link returns to the app,
and every nav entry routes correctly
- [ ] With the new layout disabled: `/admin/*` still renders the legacy
sidebar under the top Navbar
- [ ] Navigate into `/build` — sidebar collapses; re-open it manually
and it stays open; navigate away and the previous state is restored;
hard-reload on `/build` starts collapsed
- [ ] `/copilot` on mobile/tablet: no duplicate floating buttons,
sidebar toggle + workspace-files toggle both reachable in the header,
banners are not clipped
- [ ] `/library/agents/[id]` at `lg`+: detail panel and runs list scroll
independently with no page-level overflow; still fine at mobile width
- [ ] `/marketplace` and `/marketplace/search?...` have no horizontal
scrollbar next to the open sidebar
- [ ] Visit each legacy `/profile/*` URL with the new layout on —
redirects to the matching `/settings/*` page with no flash of the old
shell
- [ ] With `chat-pinning` on: pin a chat → it moves to the Pinned
section; unpin → it returns to its date/expert group; clicking a chat
shows the inline spinner
- [ ] Wallet popover: completed earn groups render collapsed with
"Done", in-progress groups render expanded with the remaining total,
headers toggle
- [ ] `pnpm test:unit` passes (`RecentChatItem`, `WalletCompactPanel`
suites updated)

#### For configuration changes:

- [ ] `.env.default` is updated or already compatible with my changes
- [ ] `docker-compose.yml` is updated or already compatible with my
changes
- [ ] I have included a list of my configuration changes in the PR
description (under **Changes**)

No configuration changes — frontend only. The `chat-pinning` flag and
its backend endpoint already exist on `dev`.
…tas#13904)

### Why / What / How

**Why.** The briefing-first home page needs pending reviews, the last
24h of run outcomes, in-flight and scheduled work, per-expert status,
and the 7-day activity rollup. Today that is six-plus round trips the
frontend has to fan out and stitch together itself, each with its own
loading state, and the cross-cutting rules (what counts as "needs
attention", how items are ordered, when an expert is `working` vs
`paused`) end up duplicated in the client. There is no endpoint that
answers "what should I look at right now?".

**What.** One new read-only endpoint, `GET /api/home`
(`get_home_dashboard`), returning a single `HomeDashboardResponse`.
Additive only — no existing endpoint, model, or query is touched.

**How.** A new `backend/api/features/home/` package with a thin route, a
service that loads every source concurrently, and pure composers that
turn that raw data into the payload:

- `service.py` fans out experts, week-window executions, pending
reviews, the cost summary, schedules, credits and the user's timezone as
concurrent tasks, then resolves library agent refs for every graph id
seen in executions or reviews. Schedules and credits are best-effort: a
scheduler or billing hiccup degrades those sections instead of failing
the whole page.
- `compose.py` + `attention.py` / `activity.py` / `agents.py` are pure
functions over that data, so the ranking and status rules are
unit-testable without a database.
- Attention items are ordered high priority first, then oldest first. A
review flips to high after 24h unacknowledged. The full
`PendingHumanReviewModel` rides along on each approval item so the
frontend can approve in one tap without a second fetch.
- Expert status uses a fixed precedence: `working` > `paused` >
`needs_setup` > `failed` > `ready`.

### Changes 🏗️

- **New** `backend/api/features/home/` — `routes`, `models`, `service`,
`compose`, `attention`, `activity`, `agents`, `helpers` (each file well
under the 300-line guideline).
- **New** `GET /api/home`, org-aware: resolves `team_ids` via
`get_user_team_ids` only when the request carries an `org_id`.
- Registered the router in `rest_api.py` and spliced the new path plus
the twelve `Home*` schemas into `openapi.json` (370 insertions, 0
deletions).
- `HomeWeekSummary` exposes `timed_run_count` alongside
`total_runtime_seconds`, so an average run duration divides by the runs
that actually contributed walltime rather than by `run_count` — which
also counts queued, running, and stats-less rows and would bias the
average down.
- Fixed a singular/plural bug while writing the tests: the setup item
rendered "1 scheduled workflow **need** setup".

### 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:
  - [x] `poetry run pytest backend/api/features/home/` — 15 tests pass
- [x] `poetry run pytest backend/data/execution_cost_summary_test.py` —
5 tests pass (base branch, unaffected)
  - [x] `poetry run format` and `poetry run lint` clean
- [x] Route returns a single payload for a personal user and for an org
user; team lookup is skipped without an `org_id`
- [x] Attention ordering, the 24h priority flip, and the credits item
only appearing on a zero balance *with* schedules
- [x] Briefing 24h window filtering, failures listed before successes,
unlisted successes counted as `routine_count`
  - [x] Agent status precedence and the team summary rollup
- [x] Week summary maps status counts, walltime and credits, including
the daily breakdown

Test coverage is on the pure composers and the route boundary.
`service.py`'s fan-out is thin glue over already-tested data functions;
the degradation paths (`_get_schedules`, `_get_credits`) are try/except
returning `[]` / `None`.

#### 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.
…avitas#14002)

## Problem

CLA-assistant re-verifies the `license/cla` status **per commit SHA**,
not per person/PR. For an already-signed contributor, that status can
transiently flip through pending/failure states before settling back to
success on every push.

`cla-label-sync.yml` (introduced in Significant-Gravitas#12002) reacts to every `status`
event, so it was flipping `cla: signed` off and `cla: pending` on for
these transient states, then flipping back moments later — visible as
rapid label churn on PRs. Reported by @Bentlybro on Significant-Gravitas#13999.

## Fix

Add a `DEMOTE_DEBOUNCE_SECONDS` (default 75s) debounce before demoting a
PR that's currently labeled `cla: signed`:

- If the latest CLA check for a previously-signed PR doesn't pass, wait
`DEMOTE_DEBOUNCE_SECONDS` and re-check once before actually removing
`cla: signed` / adding `cla: pending`.
- If the re-check now passes, skip the demotion — this filters the
same-commit transient noise.
- If it's still failing after the delay, demote as before. This still
correctly handles a genuinely new unsigned commit (e.g. pushed under a
different git identity), since that state persists well past the
debounce window rather than resolving.

Promotions (`cla: pending` → `cla: signed`) are unaffected and remain
instant.

## Why this approach

Considered "once signed, never auto-demote except via the scheduled
sweep," but that would mask a new commit pushed under a different,
unsigned identity for up to a day. The debounce preserves correctness
for that case while removing the flip-flop noise for the common
transient case.

cc @ntindle @0ubbe — flagging since you were involved in Significant-Gravitas#12002.

---------

Co-authored-by: AutoGPT <257977679+Otto-AGPT@users.noreply.github.com>
…3994)

### Why / What / How

The single-container distribution is built and validated on `dev`, but
it needs a production Docker Hub release path. This publishes only the
exact multi-architecture image content that passed the complete
appliance smoke and security scans, and gives self-hosters a usable
first-run account flow.

Manual publication from `dev` uses the reviewer-gated
`dockerhub-publish` environment and creates only an immutable
`sha-${GITHUB_SHA}` tag. A stable GitHub release named
`autogpt-platform-beta-vX.Y.Z` automatically uses the tag-restricted
`dockerhub-release` environment and creates that SHA tag, immutable
`vX.Y.Z`, and `latest` without a separate deployment approval.
Prereleases, malformed platform tags, other releases, and non-`dev`
dispatches cannot reach the Docker credentials or push jobs.

Native AMD64 and ARM64 jobs push by digest, pull those exact registry
digests back, rerun the full smoke and Trivy scans, and pass only the
verified digests to manifest assembly. Existing tags are reusable only
when they contain the exact tested digests and source revision. Registry
reads are retried with bounded validation, release tags are immutable,
and `latest` cannot move backward or replace the same version with
different content.

The successful stable-release manifest job also syncs the checked-in
single-container README to the Docker Hub Overview. The full workflow
must be present on the default branch before the next GitHub release is
published so GitHub can deliver the release event.

Fresh single-container installs start with account registration open,
matching the frontend default and making literal `docker run` usable.
Startup and documentation warn that anyone who can reach the app can
register until the operator promotes the intended administrator and
recreates the container with `AUTH_ALLOW_NEW_ACCOUNTS=false`. Canonical
Docker commands bind only to localhost.

### Changes 🏗️

- Publish native `linux/amd64` and `linux/arm64` images through separate
manual and stable-release environments.
- Publish immutable `sha-*` tags from approved manual `dev` dispatches
without changing `latest`.
- Map stable `autogpt-platform-beta-vX.Y.Z` GitHub releases
automatically to immutable `vX.Y.Z` plus monotonic `latest`.
- Fail closed before credential use for unsupported events, refs,
prereleases, and malformed tags.
- Verify exact smoke-tested child digests, OCI source revisions,
manifest platforms, repository visibility, tag immutability, and release
ordering.
- Retry registry digest resolution after writes and reject missing or
malformed digest responses.
- Keep publication policy and fixture coverage in a standalone
ShellCheck-tested helper.
- Sync a checked-in quick start, tag guide, resource guidance, and
FalkorDB memory description to Docker Hub.
- Open first-run signup by default, with explicit warnings and a tested
`false` override.

### 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:
- [x] `actionlint
.github/workflows/platform-single-container-docker.yml`
- [x] ShellCheck and `bash -n` for both changed publication/smoke
scripts
- [x] Publication fixtures cover authorization, malformed tags, Buildx
metadata forms, attestations, repository visibility, bounded registry
retries, empty optional paths, release annotations, semantic-version
ordering, and rollback rejection
- [x] 59/59 single-container appliance unit tests, including
default-open and explicit-closed signup behavior
- [x] Bake target resolves the current dedicated single-container
Dockerfile and linked backend target
- [x] Protected canary run
[31454312554](https://github.com/Significant-Gravitas/AutoGPT/actions/runs/31454312554)
built, pushed, pulled, fully smoked, scanned, and assembled both
architectures without changing `latest`
- [x] Final canary run
[31519892189](https://github.com/Significant-Gravitas/AutoGPT/actions/runs/31519892189)
passed both native pre-push and exact-registry-digest smoke/scan lanes,
published the correct attested manifest, and exposed the post-write
verification race now covered by regression tests and a successful
live-manifest replay
- [x] Public canary manifest was pulled and remained healthy locally on
ARM64 and on Unraid AMD64 using persistent named volumes
- [x] Disposable-account parity flow: signup/sign-in, real two-block
Builder execution, actionable missing-provider error from the normal
stream API, signup closure, existing-account sign-in, and rejected
second signup
- [x] Current-head PR CI builds, smokes, and scans natively on AMD64 and
ARM64
- [ ] Full workflow replaces the inert default-branch registration frame
before the next stable GitHub release

#### For configuration changes:

- [x] `.env.example` documents the open-first-run account policy and how
to close it
- [x] No Compose files are changed
- [x] No shared backend service source is changed
- [x] Configuration changes are listed under **Changes**
…nificant-Gravitas#13761)

> [!IMPORTANT]
> Preview release for user-owned ChatGPT/Codex usage. `LOCAL`
deployments allow it for every authenticated user. `CLOUD` deployments
allow it for AutoGPT `MAX`, `BUSINESS`, and `ENTERPRISE` users. There is
no Codex feature flag or staff-only allowlist, so deploying this branch
to hosted production exposes discovery, sign-in, and execution to every
user in those tiers. The connected ChatGPT account's own plan, workspace
policy, allowance, and credits still determine what Codex can execute.

### Why / What / How

This PR adds a user-funded ChatGPT connection as a distinct Codex
subscription transport, without requiring an OpenAI API key.

| Supported surface | Execution path |
|---|---|
| AutoPilot chat | Existing Claude Agent SDK and bundled Claude Code
CLI, with a loopback Anthropic Messages gateway translating text and
tool rounds to Codex App Server |
| AutoPilot graph block | The same AutoPilot path when a ChatGPT
credential is explicitly selected |
| Code Generation block | Direct Codex App Server invocation when `Codex
App Server` is selected |

Existing platform-funded, self-hosted, and OpenAI API-key routes remain
available and keep their existing behavior.

The frontend presents **Sign in with ChatGPT** under the existing OpenAI
integration card and uses OpenAI branding. The stored credential and
backend auth provider remain `codex`, while the API-key method remains
`openai`. Keeping those canonical identities separate prevents a ChatGPT
subscription credential from being mistaken for an OpenAI API key.

### Changes 🏗️

#### Authentication and credentials

- Add ChatGPT device-code sign-in, login status and cancellation,
authenticated account and rate-limit read endpoints, and disconnect with
provider logout where available. This PR does not add a frontend
plan/rate-limit dashboard.
- Store one active user-owned Codex connection per user in the existing
encrypted `IntegrationCredential` payload as
`OAuth2Credentials(provider="codex",
refresh_strategy="provider_runtime")`.
- Strictly validate ChatGPT auth bundles and reject direct API creation
of Codex or provider-runtime OAuth credentials.
- Let the Codex runtime own token refresh, then checkpoint rotated auth
back into the encrypted credential while holding the existing
distributed credential lease.
- Materialize `auth.json` only inside an isolated temporary
`CODEX_HOME`. RabbitMQ messages, Redis login state, chat sessions, and
Claude CLI environments contain credential IDs or short-lived
capabilities, not ChatGPT access, refresh, or ID tokens.
- Starting another sign-in supersedes the previous AutoGPT attempt
through Redis-backed state. Popup close, timeout, unmount, explicit
abort, and blocked-popup paths request server cancellation exactly once;
successful completion does not. This clears AutoGPT-owned stale state
but cannot bypass OpenAI-side device-code throttling or cooldowns.
- Recheck entitlement before persisting a completed login and recheck
authoritative credential ownership, provider, type, and validity at
execution boundaries.

#### Runtime and execution

- Pin `openai-codex==0.144.4` together with its bundled Codex runtime.
- Add bounded App Server capacity, startup, control, invocation, turn,
and tool timeouts; lease heartbeats; refresh checkpointing; redacted
errors; and temporary-home cleanup on success, failure, timeout,
cancellation, or shutdown.
- Keep every Codex-backed AutoPilot mode flowing through the existing
Claude Agent SDK and bundled Claude Code CLI. A request-scoped gateway
bound to `127.0.0.1` translates Claude text and MCP tool rounds to Codex
App Server `dynamicTools`. Claude Code retains the MCP tool loop,
permission/security hooks, transcripts and resume, compaction,
attachments, and sub-sessions. The CLI receives only a random per-turn
capability.
- Keep the Code Generation block separate from the Claude harness:
subscription-backed Code Generation invokes Codex App Server directly
and exposes no workspace or tool access.
- Reuse one process-local Codex runtime actor per user and credential.
Overlapping AutoPilot turns reaching the same Copilot executor process
use separate Codex threads while sharing the actor and its exclusive
credential lease; canceling one borrower does not stop its siblings.

#### Routing and model behavior

- Add an authenticated `/api/chat/transports` inventory and validate
every requested route against what is currently usable for that user and
deployment.
- Keep **AutoGPT Platform** as the default on hosted deployments.
Connecting ChatGPT adds it as another route.
- Keep **Self-hosted chat** as the default when a local chat provider is
configured. Connecting ChatGPT adds it as another route.
- On a keyless self-host with one valid ChatGPT connection,
automatically use that sole route and hide the selector. With no usable
route, show setup guidance instead of inventing a platform route.
- Show the route selector only when more than one usable route exists
and refetch the inventory after pairing or disconnecting.
- Keep the selected provider and credential ID immutable for the life of
a session, including retries, queued turns, and resume. Entitlement,
ownership, and credential validity are still checked at execution time.
Downgrade, deletion, revocation, or malformed state makes the session
fail visibly; it never reroutes to platform funding, another credential,
or another user's account.
- Prefer these AutoPilot routes when the connected account advertises
the corresponding model and effort:

| Mode | Preferred model | Effort |
|---|---|---|
| Fast / Balanced | GPT-5.6 Luna | low |
| Fast / Advanced | GPT-5.6 Terra | medium |
| Thinking / Balanced | GPT-5.6 Terra | high |
| Thinking / Advanced | GPT-5.6 Sol | xhigh |

If a preferred model or effort is unavailable, routing uses an allowed
account-advertised default or available OpenAI model and a
supported/default effort.

For Code Generation, the model dropdown applies only to the OpenAI API
transport. The subscription transport lets App Server choose its live
account-compatible model while still applying the selected supported
reasoning effort.

#### Entitlement and cost behavior

- Add the shared `require_entitlement(user_id, entitlement)` policy
shell.
- In `LOCAL` mode, allow `codex_subscription_transport` before any
subscription database lookup.
- In `CLOUD` mode, read the authenticated user's authoritative AutoGPT
`User.subscriptionTier` through `DatabaseManager` on every check in this
PR and allow `MAX`, `BUSINESS`, and `ENTERPRISE`.
- Hide Codex from optional discovery when tier state cannot be resolved.
Actual connect and execution requests fail with a retryable
service-availability error rather than treating an outage as a valid
route.
- Keep pending-login cancellation and credential deletion available even
after downgrade so users can clean up.
- Do not separately resolve organization-seat entitlements in this PR;
cloud access is based on the user's authoritative `subscriptionTier`.
- Keep entitlement reads deliberately uncached here. Shared caching and
tier-mutation invalidation are isolated in follow-up
[Significant-Gravitas#14004](Significant-Gravitas#14004).
- Record subscription-backed token usage with
`billing_mode=user_subscription` and the execution path. Do not report
public OpenAI API USD pricing as AutoGPT provider spend.
- Skip platform model-paywall and platform-provider USD usage windows
for Codex AutoPilot. Subscription-backed Code Generation does not apply
the API-token wallet charge. Ordinary graph execution, turn concurrency,
queueing, bridge compute, and infrastructure policies still apply, and
usage consumes the connected ChatGPT account's allowance or credits.
- Preserve existing OpenAI API-key billing behavior.

### Configuration / rollout

No new secret, Codex rollout flag, or Codex-specific frontend build
argument is required.

- Cloud previews must still set `FRONTEND_BASE_URL`, build-time
`NEXT_PUBLIC_FRONTEND_BASE_URL`, and runtime `BETTER_AUTH_URL` to the
exact public origin. Otherwise the device-login popup can use the wrong
host or lack the preview authentication cookie.
- Outside the single-container appliance, `CODEX_TEMP_ROOT` is optional
and otherwise falls back to the operating-system temporary directory.
- Local Compose sets `CODEX_TEMP_ROOT=/run/autogpt-codex` and mounts a
128 MiB memory-backed temporary filesystem in the REST, graph-executor,
and Copilot-executor containers.
- The single-container appliance fixes
`CODEX_TEMP_ROOT=/dev/shm/autogpt-codex`, creates it as `0700` for the
unprivileged backend user, and verifies that REST, graph executor, and
Copilot executor inherit it. Docker's `/dev/shm` keeps auth homes out of
the writable layer; the documented quick-start sizes it to 2 GiB.
- Narrow Git attributes keep the appliance shell scripts LF-normalized
in Windows checkouts so a Windows source build uses the same executable
bytes as Linux CI.
- Capacity, process startup/control, checkpoint, invocation,
Copilot-turn, tool, and login intervals have bounded defaults and
optional `CODEX_*` overrides.
- Deployments need outbound HTTPS access to OpenAI. Device-code login
does not require an inbound OpenAI callback.

### Explicit exclusions and known limitations

This PR does not route the following through a user's ChatGPT
connection:

- Orchestrator or shared LLM blocks;
- image generation or embeddings;
- `ClaudeCodeBlock` / E2B;
- builder-panel-bound sessions;
- background or system-owned LLM work.

It also does not add credential pooling, silent fallback to a platform
credential, multiple active Codex credentials per user, per-user
Kubernetes resources, persistent Codex homes, or persistent volumes.

Operational limits that remain:

- Device-code authorization must be enabled for the ChatGPT account or
workspace. A pending AutoGPT attempt is bounded by
`CODEX_LOGIN_TIMEOUT_SECONDS`, 15 minutes by default.
- Login actors are process-local. Redis shares status, supersession, and
cancellation across REST replicas, but a restart of the owner process
interrupts the active sign-in.
- Runtime actors are process-local. A different Copilot executor process
or replica cannot join the current owner and may receive the bounded,
retryable `codex_credential_busy` error. Multi-replica owner routing or
a dedicated bridge is not included.
- Codex App Server and device-login actors currently run inside the
ordinary REST, graph-executor, and Copilot-executor containers. A
dedicated non-root, least-privilege bridge with memory-only homes,
restricted egress, process limits, and credential-owner routing remains
follow-up work.
- Commercial/policy approval for hosted subscription passthrough and the
Claude-harness compatibility route is a deployment decision outside this
implementation; this PR does not claim that approval.

### Validation

At review-fix parent `9cbe549d36dd3197b22871dbb7ae79d85c26c03f` against
`dev@5c980c2`, the formatted tree
passed the following focused validation:

- Backend auth/runtime suite: 95 passed, with 3 Windows-specific skips.
- Backend model routing, compatibility gateway, and SDK environment
suite: 74 passed.
- Backend platform-cost, metrics, and Code Generation suite: 94 passed.
- Backend billing/cost-leak suite: 19 passed.
- Authenticated integrations API suite: 25 passed.
- Isolated Linux executor/gateway suites: 65 passed, including processor
cleanup, lease ownership, Codex billing-gate, and compatibility-gateway
behavior; some cases overlap the focused Windows suites above.
- Frontend: 83 tests across five affected files, TypeScript `tsc
--noEmit`, and targeted Prettier checks pass.
- Backend isort, Black, Ruff, AST/import compilation checks, and Git
diff hygiene pass for every changed Python file.

Final head `aa4bcb445c450d2da1c0de2a4c1ea03b9c3b70e5` restores the
framework-compatible optional AutoPilot credential annotation,
regenerates the public provider-discovery OpenAPI contract, safely
canonicalizes macOS system temp aliases, and includes the exact
generated Code Generation documentation sync emitted by CI. On that
final diff, the AutoPilot schema/import assertion passes; the temp-home
suite passes 12 tests with 6 expected Windows symlink skips; Ruff,
Black, Prettier, Git diff hygiene, and full TypeScript `tsc --noEmit`
pass. The final test-only commit also updates the shutdown mock to patch
the manager module's bound transport symbol; that exact regression
passes locally. Current-head CI remains the authoritative broad
verification below.

The prior head `9eb8283db22d6ddf624f6c891c5b1579cdd07516` also completed
a real-account, keyless local single-container smoke: four Claude Agent
SDK turns used the Codex subscription route successfully, persisted 40
chat messages, recorded subscription token usage, and recorded zero
platform model cost. The review-fix head changes authentication cleanup,
model-routing structure, compatibility edge cases, cost reporting, and
selector cleanup, so exact-head external-account smoke remains
explicitly unchecked below.

- [x] Focused backend tests cover auth-bundle validation, temporary-home
isolation, refresh/checkpointing, login supersession and cancellation,
entitlement enforcement, authoritative credential identity, transport
inventory, immutable session routing, process-local runtime sharing,
queue/executor behavior, costs, Code Generation, and AutoPilot.
- [x] The bundled Claude Agent SDK/CLI conformance suite covers
streaming text, tool call/result, cancellation, and resume against the
compatibility gateway.
- [x] Frontend tests cover OpenAI/ChatGPT presentation, credential
selection, popup cancellation, transport inventory, selector defaults
and visibility, setup guidance, and session creation.
- [x] Current-head backend, frontend, full-stack, dual-architecture
single-container, CodeQL, Classic, overlap, documentation, and PR-status
automation passed on `aa4bcb445c450d2da1c0de2a4c1ea03b9c3b70e5`.
- [ ] Fresh deployed-preview device-code sign-in at the current head
with a real ChatGPT account.
- [ ] Fresh deployed-preview Code Generation and AutoPilot smoke at the
current head with a real ChatGPT account.

The unchecked real-account checks are intentionally not represented as
automated coverage; they consume an external account's allowance and
should be completed on the exact deployment selected for release.
### Checklist 📋

#### For code changes:

- [x] Changes and explicit exclusions are listed above.
- [x] Test and rollout evidence is documented above.
- [x] Local focused backend, frontend, conformance, formatting, and diff
checks finish successfully.
- [x] Credential access is user-scoped and execution revalidates
authoritative credential identity.

#### For configuration changes:

- [x] Existing environment defaults remain compatible; no new required
secret or rollout flag was added.
- [x] Compose supplies memory-backed temporary storage for local
execution.
- [x] Required cloud-preview origin settings and optional Codex runtime
settings are documented above.






<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **High Risk**
> Touches authentication, credential storage/refresh, chat session
routing, and billing/paywall behavior for a new user-funded LLM path. A
routing or entitlement bug could leak paid platform usage or expose
ChatGPT credentials incorrectly.
> 
> **Overview**
> Adds a **user-funded ChatGPT/Codex subscription transport** so
AutoPilot chat, the AutoPilot graph block, and Code Generation can run
on a connected ChatGPT plan instead of platform-funded or OpenAI API-key
routes.
> 
> **Auth & credentials.** Introduces ChatGPT device-code sign-in under
the existing OpenAI integration card, stores one `codex` OAuth
credential with runtime-owned refresh/checkpointing, and blocks direct
API creation of Codex credentials. Discovery, connect, and execution are
entitlement-gated (`LOCAL` for all users; `CLOUD` for
Max/Business/Enterprise).
> 
> **Routing & billing.** Adds `/api/chat/transports` and persists an
immutable `llm_auth_provider` / `llm_credential_id` on each session.
Codex AutoPilot skips the platform paywall and USD usage windows;
subscription usage is recorded as `user_subscription` rather than
platform spend. Builder-bound sessions stay platform-only.
> 
> **Runtime.** Pins the Codex App Server runtime, isolates auth homes
under `CODEX_TEMP_ROOT`, and reuses a process-local runtime actor under
an exclusive credential lease. AutoPilot still goes through the Claude
Agent SDK via a loopback compatibility gateway; Code Generation can
invoke App Server directly when that transport is selected.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1441864. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Bentlybro <Github@bentlybro.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ificant-Gravitas#13999)

### Why / What / How

**Why** — the Slack install flow dead-ended. After OAuth you landed on a
blank browser tab with no route back; the Bots settings page showed no
sign an install had happened (it looked untouched); and once you did
link, the success page told you to close the tab rather than return to
the bot. Every step forward left the user to find their own way to the
next one.

**What** — closes the loop: the install now hands you to the bot chat
and returns the tab to Settings → Bots, the Bots page reflects a
half-finished install, and the success page routes you back to the bot.
Plus each platform is finally called by its own name — Slack has
*workspaces*, Telegram *groups*, Discord *servers*.

**How** — the install is attributed to the signed-in user via an
HMAC-signed param folded into the OAuth state, so the (unauthenticated)
callback can record a short-lived pending marker in Redis without
trusting the query string. The callback then redirects to a small
handoff page of ours, which opens the bot DM through the Slack desktop
deep link and returns the tab to settings — keeping the whole round-trip
in one tab, so nothing is stranded. `server_noun` is declared once per
platform in the registry and flows to every surface.

### Changes 🏗️

**Backend**
- `slack/oauth.py`: signed `u` param → OAuth state → user-attributed
install; callback redirects to the handoff page (falls back to Slack's
redirect when no frontend is configured)
- `slack/pending.py` (new): Redis-backed pending-install marker, 7-day
TTL, best-effort — losing Redis degrades the hint, never the install
- `platform_linking/registry.py`: `server_noun` per platform +
`server_noun_for()` lookup
- `platform_linking/models.py`: `PendingInstallInfo`, plus `server_noun`
/ `pending_install` / `return_url` fields (all optional, defaulted)
- `platform_linking/routes.py`: user-bound install URL, pending
surfacing, `return_url` on both confirm endpoints, marker retired on DM
link

**Frontend**
- `BotCard`: pending badge + "Open AutoGPT in Slack", per-platform
section headings, install now same-tab
- `BotCardDmTile`: explains the one remaining step, echoing the
workspace name
- `BotCardServerList`: per-platform nouns; a link with no name no longer
claims the bot is missing
- `SuccessView` / `ReadyView`: return-to-bot action, roomier layout,
smaller buttons
- `link/slack/installed` (new): deep-link handoff page that returns the
tab to settings
- `useBotsList`: refetches on tab focus/visibility — the install
round-trip ends outside the app, so the page must notice you coming back
- `AuthCard`: applies its `className` prop (it was declared and silently
dropped)

**Config**: none. No `.env` or `docker-compose.yml` changes.

**API**: `openapi.json` regenerated (additive: 3 optional fields + 1
schema).

### 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:
- [x] Install from Settings → Bots: OAuth completes, Slack desktop opens
at the bot DM, tab returns to Settings → Bots
- [x] Bots page shows "Pending — finish in Slack" with the workspace
name, without a manual reload
- [x] DM the bot → Link Account → confirm → success page offers "Return
to Slack", which lands back in the DM
  - [x] Pending badge clears once the DM is linked (marker retired)
- [x] Section headings read "Linked workspaces" (Slack), "Linked groups"
(Telegram), "Linked servers" (Discord)
- [x] Discord and Telegram cards render unchanged where the new fields
are absent
  - [x] Frontend tests: 25/25 pass (`settings/bots`, `link/[token]`)
- [x] Backend: ruff + black clean; new/updated tests cover state and
`u`-param round-trips, tamper and expiry, pending recording, both
redirect paths, `return_url` on both confirms, and non-Slack platforms
untouched

Verified end-to-end against a live Slack workspace, not just mocks.

#### 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**)

### Notes for reviewers

- **Backend tests couldn't be run locally.** Current `dev` makes
`graph_cleanup` a session-autouse fixture depending on `SpinTestServer`,
so every backend test boots the full service stack — which can't come up
on a Windows host. The Python tests here are written but unverified
until CI runs them.
- The pending marker is deliberately **Redis, not a table**: it's
transient journey state, not a record. It fails open — if Redis is
unavailable the hint disappears but the install and link still work.
- **Slack is the only platform with the full loop**, because it's the
only one whose install round-trips our backend. Discord invites,
Telegram deep links and Teams sideloads never touch us, so we can't know
an install happened. The UI treatment is generic, so any platform that
can populate the fields gets it for free.
… layouts (Significant-Gravitas#14000)

### Why / What / How

**Why:** The legacy `/profile/*` pages redirect to their `/settings/*`
replacements, but only when the `autogpt-new-layout` flag is on — and
the redirect drops the hash and query string. Two things break as a
result:

1. **Notification unsubscribe emails 404 in spirit.** Every notification
email links to `/profile/settings#notifications`
(`backend/notifications/email.py:79`,
`notifications/notifications.py:788`, also used for the
`List-Unsubscribe` header). The `#notifications` anchor was silently
discarded on the hop, and classic-layout users were never redirected at
all.
2. **Stripe return params were eaten.** `PaywallGate` sends Stripe to
`/profile/credits?subscription=success` (`usePaywallModal.ts:106`); the
query string never reached `/settings/billing`, so the success toast at
`settings/billing/page.tsx:97` could never fire.

The `/settings/*` pages render fine under both the classic and the new
shell — only the chrome differs (`PlatformChrome.tsx:118,128-135`) — so
gating the redirect on the layout flag was never necessary.

**What:** Ungate the redirect and preserve `search` + `hash` across it.
Keep `/profile/oauth-apps` on the legacy page.

**How:** `useNewSettingsRedirect` no longer reads `usePlatformChrome`;
it redirects on any `/profile/*` pathname. Since `usePathname()` never
carries the fragment, the effect reads `window.location` at redirect
time and appends both parts to `router.replace()`. A client-side
redirect is the correct mechanism here — a server redirect cannot see
`#hash` at all, because browsers never send it.

Dropping the `isMounted` gate does not reintroduce a hydration mismatch:
`layout.tsx` renders `null` for redirecting paths on both server and
client, so the two agree.

The legacy pages are **not** removed — they stay routable and simply
redirect, per the requirement that old links must never 404.

### Changes 🏗️

**`useNewSettingsRedirect.ts`**
- Removed the `isNewLayoutActive` gate — legacy links now redirect under
both layouts
- Redirect target is now `` `${newPath}${search}${hash}` ``, so
`/profile/settings#notifications` → `/settings/account#notifications`
and `/profile/credits?subscription=success` →
`/settings/billing?subscription=success`
- Added `KEEP_ON_LEGACY` with `/profile/oauth-apps`:
`/settings/oauth-apps` is still a `"Coming soon."` placeholder
(`settings/oauth-apps/page.tsx:12`) and isn't in the settings sidebar,
so redirecting there would take a working page away from every user.
One-line removal once the real page ships.
- Guarded on `pathname.startsWith("/profile")` so the fallback can't
fire outside the legacy tree

**`profile/(user)/layout.tsx`**
- Comment updated — the redirect is no longer new-layout-specific

**Tests** (`__tests__/useNewSettingsRedirect.test.tsx`)
- Dropped the `usePlatformChrome` mock and the "does not redirect while
the classic layout is active" case, which asserted the behaviour this PR
removes
- Added: `#notifications` anchor preserved; `?subscription=success`
preserved; `/profile/oauth-apps` stays put; non-`/profile` paths are
untouched

**Out of scope / follow-ups**
- `/settings/account` still does not render `NotificationsCard` (hidden
since Significant-Gravitas#12976 pending copy/UX). The redirect now lands correctly, but
there are still no notification toggles on the other side — deliberately
left for the follow-up.
- `backend/notifications/email.py:79` could append `#notifications` to
the unsubscribe link so recipients land directly on the toggles. Backend
change, separate PR.
- `PreferencesSkeleton.tsx:13` still renders a notifications placeholder
that vanishes on load.

### 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:
- [x] `pnpm test:unit` — `useNewSettingsRedirect.test.tsx` passes (9
cases)
  - [x] `pnpm types` clean, `pnpm format` / `pnpm lint` clean
- [ ] With **no** feature flag set, open
`/profile/settings#notifications` → lands on
`/settings/account#notifications`
- [ ] With `NEXT_PUBLIC_FORCE_FLAG_AUTOGPT_NEW_LAYOUT=true`, same URL →
same result
- [ ] `/profile`, `/profile/dashboard`, `/profile/credits`,
`/profile/integrations`, `/profile/api-keys` each land on their
`/settings/*` counterpart
- [ ] `/profile/credits?subscription=success` →
`/settings/billing?subscription=success`
  - [ ] `/profile/oauth-apps` still renders the legacy OAuth apps page
  - [ ] `/profile/does-not-exist` → `/settings/profile`
  - [ ] No flash of the legacy sidebar shell before the redirect

#### 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
# Conflicts:
#	.github/workflows/platform-single-container-docker.yml
…questBlock

The content-type check previously only matched 'application/json' exactly, causing responses with types like 'application/vnd.api+json', 'application/hal+json', etc. to be returned as raw strings instead of parsed JSON objects.

Fix: also match content types ending with '+json' per RFC 6839.

Fixes Significant-Gravitas#14007
- Make credentials Optional in llm_call, _llm_call, and all AIStructuredResponseGeneratorBlock methods
- Handle None credentials gracefully (empty string for Ollama/local models)
- Update AICredentialsField to allow null defaults
- Replace FormCreator with textarea in StickyNoteBlock
- Add cursor-position preservation in StickyNoteBlock useCustomNode

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Use ensure_ascii=True in json.dumps() when serializing variables into
the AGPT_VARIABLES env var. Without this, surrogate pairs in emoji
strings cause os.environ.__setitem__ to raise UnicodeEncodeError
on Python 3.13, crashing ExecuteCodeBlock before user code runs.

Closes Significant-Gravitas#13551
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants